##// END OF EJS Templates
merge default to stable for 0.4.0
Thomas De Schampheleire -
r7530:19af3fef merge stable
parent child Browse files
Show More

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

@@ -0,0 +1,33 b''
1 [run]
2 omit =
3 # the bin scripts are not part of the Kallithea web app
4 kallithea/bin/*
5 # we ship with no active extensions
6 kallithea/config/rcextensions/*
7 # dbmigrate is not a part of the Kallithea web app
8 kallithea/lib/dbmigrate/*
9 # the tests themselves should not be part of the coverage report
10 kallithea/tests/*
11 # the scm hooks are not run in the kallithea process
12 kallithea/config/post_receive_tmpl.py
13 kallithea/config/pre_receive_tmpl.py
14
15 # same omit lines should be present in sections 'run' and 'report'
16 [report]
17 omit =
18 # the bin scripts are not part of the Kallithea web app
19 kallithea/bin/*
20 # we ship with no active extensions
21 kallithea/config/rcextensions/*
22 # dbmigrate is not a part of the Kallithea web app
23 kallithea/lib/dbmigrate/*
24 # the tests themselves should not be part of the coverage report
25 kallithea/tests/*
26 # the scm hooks are not run in the kallithea process
27 kallithea/config/post_receive_tmpl.py
28 kallithea/config/pre_receive_tmpl.py
29
30 [paths]
31 source =
32 kallithea/
33 **/workspace/*/kallithea
@@ -0,0 +1,212 b''
1 def createvirtualenv = ''
2 def activatevirtualenv = ''
3
4 node {
5 properties([[$class: 'BuildDiscarderProperty',
6 strategy: [$class: 'LogRotator',
7 artifactDaysToKeepStr: '',
8 artifactNumToKeepStr: '10',
9 daysToKeepStr: '',
10 numToKeepStr: '']]]);
11 if (isUnix()) {
12 createvirtualenv = 'rm -r $JENKINS_HOME/venv/$JOB_NAME || true && virtualenv $JENKINS_HOME/venv/$JOB_NAME'
13 activatevirtualenv = '. $JENKINS_HOME/venv/$JOB_NAME/bin/activate'
14 } else {
15 createvirtualenv = 'rmdir /s /q %JENKINS_HOME%\\venv\\%JOB_NAME% || true && virtualenv %JENKINS_HOME%\\venv\\%JOB_NAME%'
16 activatevirtualenv = 'call %JENKINS_HOME%\\venv\\%JOB_NAME%\\Scripts\\activate.bat'
17 }
18
19 stage('checkout') {
20 checkout scm
21 if (isUnix()) {
22 sh 'hg --config extensions.purge= purge --all'
23 } else {
24 bat 'hg --config extensions.purge= purge --all'
25 }
26 }
27 stage('virtual env') {
28 def virtualenvscript = """$createvirtualenv
29 $activatevirtualenv
30 python -m pip install --upgrade pip
31 pip install --upgrade setuptools
32 pip install --upgrade pylint
33 pip install --upgrade pytest-cov
34 """
35 if (isUnix()) {
36 virtualenvscript += """
37 pip install --upgrade python-ldap
38 pip install --upgrade python-pam
39 """
40 sh virtualenvscript
41 } else {
42 bat virtualenvscript
43 }
44 }
45 stage('setup') {
46 def virtualenvscript = """$activatevirtualenv
47 pip install --upgrade -e .
48 pip install -r dev_requirements.txt
49 python setup.py compile_catalog
50 """
51 if (isUnix()) {
52 sh virtualenvscript
53 } else {
54 bat virtualenvscript
55 }
56 stash name: 'kallithea', useDefaultExcludes: false
57 }
58 stage('pylint') {
59 sh script: """$activatevirtualenv
60 pylint -j 0 --disable=C -f parseable kallithea > pylint.out
61 """, returnStatus: true
62 archiveArtifacts 'pylint.out'
63 try {
64 step([$class: 'WarningsPublisher', canComputeNew: false, canResolveRelativePaths: false, defaultEncoding: '', excludePattern: '', healthy: '', includePattern: '', messagesPattern: '', parserConfigurations: [[parserName: 'PyLint', pattern: 'pylint.out']], unHealthy: ''])
65 } catch (java.lang.IllegalArgumentException exc) {
66 echo "You need to install the 'Warnings Plug-in' to display the pylint report."
67 currentBuild.result = 'UNSTABLE'
68 echo "Caught: ${exc}"
69 }
70 }
71 }
72
73 def pytests = [:]
74 pytests['sqlite'] = {
75 node {
76 ws {
77 deleteDir()
78 unstash name: 'kallithea'
79 if (isUnix()) {
80 sh script: """$activatevirtualenv
81 py.test -p no:sugar --cov-config .coveragerc --junit-xml=pytest_sqlite.xml --cov=kallithea
82 """, returnStatus: true
83 } else {
84 bat script: """$activatevirtualenv
85 py.test -p no:sugar --cov-config .coveragerc --junit-xml=pytest_sqlite.xml --cov=kallithea
86 """, returnStatus: true
87 }
88 sh 'sed --in-place "s/\\(classname=[\'\\"]\\)/\\1SQLITE./g" pytest_sqlite.xml'
89 archiveArtifacts 'pytest_sqlite.xml'
90 junit 'pytest_sqlite.xml'
91 writeFile(file: '.coverage.sqlite', text: readFile('.coverage'))
92 stash name: 'coverage.sqlite', includes: '.coverage.sqlite'
93 }
94 }
95 }
96
97 pytests['de'] = {
98 node {
99 if (isUnix()) {
100 ws {
101 deleteDir()
102 unstash name: 'kallithea'
103 withEnv(['LANG=de_DE.UTF-8',
104 'LANGUAGE=de',
105 'LC_ADDRESS=de_DE.UTF-8',
106 'LC_IDENTIFICATION=de_DE.UTF-8',
107 'LC_MEASUREMENT=de_DE.UTF-8',
108 'LC_MONETARY=de_DE.UTF-8',
109 'LC_NAME=de_DE.UTF-8',
110 'LC_NUMERIC=de_DE.UTF-8',
111 'LC_PAPER=de_DE.UTF-8',
112 'LC_TELEPHONE=de_DE.UTF-8',
113 'LC_TIME=de_DE.UTF-8',
114 ]) {
115 sh script: """$activatevirtualenv
116 py.test -p no:sugar --cov-config .coveragerc --junit-xml=pytest_de.xml --cov=kallithea
117 """, returnStatus: true
118 }
119 sh 'sed --in-place "s/\\(classname=[\'\\"]\\)/\\1DE./g" pytest_de.xml'
120 archiveArtifacts 'pytest_de.xml'
121 junit 'pytest_de.xml'
122 writeFile(file: '.coverage.de', text: readFile('.coverage'))
123 stash name: 'coverage.de', includes: '.coverage.de'
124 }
125 }
126 }
127 }
128 pytests['mysql'] = {
129 node {
130 if (isUnix()) {
131 ws {
132 deleteDir()
133 unstash name: 'kallithea'
134 sh """$activatevirtualenv
135 pip install --upgrade MySQL-python
136 """
137 withEnv(['TEST_DB=mysql://kallithea:kallithea@jenkins_mysql/kallithea_test?charset=utf8']) {
138 if (isUnix()) {
139 sh script: """$activatevirtualenv
140 py.test -p no:sugar --cov-config .coveragerc --junit-xml=pytest_mysql.xml --cov=kallithea
141 """, returnStatus: true
142 } else {
143 bat script: """$activatevirtualenv
144 py.test -p no:sugar --cov-config .coveragerc --junit-xml=pytest_mysql.xml --cov=kallithea
145 """, returnStatus: true
146 }
147 }
148 sh 'sed --in-place "s/\\(classname=[\'\\"]\\)/\\1MYSQL./g" pytest_mysql.xml'
149 archiveArtifacts 'pytest_mysql.xml'
150 junit 'pytest_mysql.xml'
151 writeFile(file: '.coverage.mysql', text: readFile('.coverage'))
152 stash name: 'coverage.mysql', includes: '.coverage.mysql'
153 }
154 }
155 }
156 }
157 pytests['postgresql'] = {
158 node {
159 if (isUnix()) {
160 ws {
161 deleteDir()
162 unstash name: 'kallithea'
163 sh """$activatevirtualenv
164 pip install --upgrade psycopg2
165 """
166 withEnv(['TEST_DB=postgresql://kallithea:kallithea@jenkins_postgresql/kallithea_test']) {
167 if (isUnix()) {
168 sh script: """$activatevirtualenv
169 py.test -p no:sugar --cov-config .coveragerc --junit-xml=pytest_postgresql.xml --cov=kallithea
170 """, returnStatus: true
171 } else {
172 bat script: """$activatevirtualenv
173 py.test -p no:sugar --cov-config .coveragerc --junit-xml=pytest_postgresql.xml --cov=kallithea
174 """, returnStatus: true
175 }
176 }
177 sh 'sed --in-place "s/\\(classname=[\'\\"]\\)/\\1POSTGRES./g" pytest_postgresql.xml'
178 archiveArtifacts 'pytest_postgresql.xml'
179 junit 'pytest_postgresql.xml'
180 writeFile(file: '.coverage.postgresql', text: readFile('.coverage'))
181 stash name: 'coverage.postgresql', includes: '.coverage.postgresql'
182 }
183 }
184 }
185 }
186 stage('Tests') {
187 parallel pytests
188 node {
189 unstash 'coverage.sqlite'
190 unstash 'coverage.de'
191 unstash 'coverage.mysql'
192 unstash 'coverage.postgresql'
193 if (isUnix()) {
194 sh script: """$activatevirtualenv
195 coverage combine .coverage.sqlite .coverage.de .coverage.mysql .coverage.postgresql
196 coverage xml
197 """, returnStatus: true
198 } else {
199 bat script: """$activatevirtualenv
200 coverage combine .coverage.sqlite .coverage.de .coverage.mysql .coverage.postgresql
201 coverage xml
202 """, returnStatus: true
203 }
204 try {
205 step([$class: 'CoberturaPublisher', autoUpdateHealth: false, autoUpdateStability: false, coberturaReportFile: 'coverage.xml', failNoReports: false, failUnhealthy: false, failUnstable: false, maxNumberOfBuilds: 0, onlyStable: false, zoomCoverageChart: false])
206 } catch (java.lang.IllegalArgumentException exc) {
207 echo "You need to install the pipeline compatible 'CoberturaPublisher Plug-in' to display the coverage report."
208 currentBuild.result = 'UNSTABLE'
209 echo "Caught: ${exc}"
210 }
211 }
212 }
@@ -0,0 +1,8 b''
1 pytest >= 3.3.0, < 3.8
2 pytest-runner < 4.3
3 pytest-sugar >= 0.7.0, < 0.10
4 pytest-benchmark < 3.2
5 pytest-localserver < 0.5
6 mock < 2.1
7 Sphinx < 1.8
8 WebTest < 2.1
@@ -0,0 +1,74 b''
1 =======================
2 Database schema changes
3 =======================
4
5 Kallithea uses Alembic for :ref:`database migrations <upgrade_db>`
6 (upgrades and downgrades).
7
8 If you are developing a Kallithea feature that requires database schema
9 changes, you should make a matching Alembic database migration script:
10
11 1. :ref:`Create a Kallithea configuration and database <setup>` for testing
12 the migration script, or use existing ``development.ini`` setup.
13
14 Ensure that this database is up to date with the latest database
15 schema *before* the changes you're currently developing. (Do not
16 create the database while your new schema changes are applied.)
17
18 2. Create a separate throwaway configuration for iterating on the actual
19 database changes::
20
21 kallithea-cli config-create temp.ini
22
23 Edit the file to change database settings. SQLite is typically fine,
24 but make sure to change the path to e.g. ``temp.db``, to avoid
25 clobbering any existing database file.
26
27 3. Make your code changes (including database schema changes in ``db.py``).
28
29 4. After every database schema change, recreate the throwaway database
30 to test the changes::
31
32 rm temp.db
33 kallithea-cli db-create -c temp.ini --repos=/var/repos --user=doe --email doe@example.com --password=123456 --no-public-access --force-yes
34 kallithea-cli repo-scan -c temp.ini
35
36 5. Once satisfied with the schema changes, auto-generate a draft Alembic
37 script using the development database that has *not* been upgraded.
38 (The generated script will upgrade the database to match the code.)
39
40 ::
41
42 alembic -c development.ini revision -m "area: add cool feature" --autogenerate
43
44 6. Edit the script to clean it up and fix any problems.
45
46 Note that for changes that simply add columns, it may be appropriate
47 to not remove them in the downgrade script (and instead do nothing),
48 to avoid the loss of data. Unknown columns will simply be ignored by
49 Kallithea versions predating your changes.
50
51 7. Run ``alembic -c development.ini upgrade head`` to apply changes to
52 the (non-throwaway) database, and test the upgrade script. Also test
53 downgrades.
54
55 The included ``development.ini`` has full SQL logging enabled. If
56 you're using another configuration file, you may want to enable it
57 by setting ``level = DEBUG`` in section ``[handler_console_sql]``.
58
59 The Alembic migration script should be committed in the same revision as
60 the database schema (``db.py``) changes.
61
62 See the `Alembic documentation`__ for more information, in particular
63 the tutorial and the section about auto-generating migration scripts.
64
65 .. __: http://alembic.zzzcomputing.com/en/latest/
66
67
68 Troubleshooting
69 ---------------
70
71 * If ``alembic --autogenerate`` responds "Target database is not up to
72 date", you need to either first use Alembic to upgrade the database
73 to the most recent version (before your changes), or recreate the
74 database from scratch (without your schema changes applied).
@@ -0,0 +1,2 b''
1 .. _translations:
2 .. include:: ./../../kallithea/i18n/how_to
@@ -0,0 +1,246 b''
1 .. _upgrade:
2
3 ===================
4 Upgrading Kallithea
5 ===================
6
7 This describes the process for upgrading Kallithea, independently of the
8 Kallithea installation method.
9
10 .. note::
11 If you are upgrading from a RhodeCode installation, you must first
12 install Kallithea 0.3.2 and follow the instructions in the 0.3.2
13 README to perform a one-time conversion of the database from
14 RhodeCode to Kallithea, before upgrading to the latest version
15 of Kallithea.
16
17
18 1. Stop the Kallithea web application
19 -------------------------------------
20
21 This step depends entirely on the web server software used to serve
22 Kallithea, but in any case, Kallithea should not be running during
23 the upgrade.
24
25 .. note::
26 If you're using Celery, make sure you stop all instances during the
27 upgrade.
28
29
30 2. Create a backup of both database and configuration
31 -----------------------------------------------------
32
33 You are of course strongly recommended to make backups regularly, but it
34 is *especially* important to make a full database and configuration
35 backup before performing a Kallithea upgrade.
36
37 Back up your configuration
38 ^^^^^^^^^^^^^^^^^^^^^^^^^^
39
40 Make a copy of your Kallithea configuration (``.ini``) file.
41
42 If you are using :ref:`rcextensions <customization>`, you should also
43 make a copy of the entire ``rcextensions`` directory.
44
45 Back up your database
46 ^^^^^^^^^^^^^^^^^^^^^
47
48 If using SQLite, simply make a copy of the Kallithea database (``.db``)
49 file.
50
51 If using PostgreSQL, please consult the documentation for the ``pg_dump``
52 utility.
53
54 If using MySQL, please consult the documentation for the ``mysqldump``
55 utility.
56
57 Look for ``sqlalchemy.url`` in your configuration file to determine
58 database type, settings, location, etc. If you were running Kallithea 0.3.x or
59 older, this was ``sqlalchemy.db1.url``.
60
61
62 3. Activate or recreate the Kallithea virtual environment (if any)
63 ------------------------------------------------------------------
64
65 .. note::
66 If you did not install Kallithea in a virtual environment, skip this step.
67
68 For major upgrades, e.g. from 0.3.x to 0.4.x, it is recommended to create a new
69 virtual environment, rather than reusing the old. For minor upgrades, e.g.
70 within the 0.4.x range, this is not really necessary (but equally fine).
71
72 To create a new virtual environment, please refer to the appropriate
73 installation page for details. After creating and activating the new virtual
74 environment, proceed with the rest of the upgrade process starting from the next
75 section.
76
77 To reuse the same virtual environment, first activate it, then verify that you
78 are using the correct environment by running::
79
80 pip freeze
81
82 This will list all packages installed in the current environment. If
83 Kallithea isn't listed, deactivate the environment and then activate the correct
84 one, or recreate a new environment. See the appropriate installation page for
85 details.
86
87
88 4. Install new version of Kallithea
89 -----------------------------------
90
91 Please refer to the instructions for the installation method you
92 originally used to install Kallithea.
93
94 If you originally installed using pip, it is as simple as::
95
96 pip install --upgrade kallithea
97
98 If you originally installed from version control, assuming you did not make
99 private changes (in which case you should adapt the instructions accordingly)::
100
101 cd my-kallithea-clone
102 hg parent # make a note of the original revision
103 hg pull
104 hg update
105 hg parent # make a note of the new revision
106 pip install --upgrade -e .
107
108 .. _upgrade_config:
109
110
111 5. Upgrade your configuration
112 -----------------------------
113
114 Run the following command to create a new configuration (``.ini``) file::
115
116 kallithea-cli config-create new.ini
117
118 Then compare it with your old config file and copy over the required
119 configuration values from the old to the new file.
120
121 .. note::
122 Please always make sure your ``.ini`` files are up to date. Errors
123 can often be caused by missing parameters added in new versions.
124
125 .. _upgrade_db:
126
127
128 6. Upgrade your database
129 ------------------------
130
131 .. note::
132 If you are *downgrading* Kallithea, you should perform the database
133 migration step *before* installing the older version. (That is,
134 always perform migrations using the most recent of the two versions
135 you're migrating between.)
136
137 First, run the following command to see your current database version::
138
139 alembic -c new.ini current
140
141 Typical output will be something like "9358dc3d6828 (head)", which is
142 the current Alembic database "revision ID". Write down the entire output
143 for troubleshooting purposes.
144
145 The output will be empty if you're upgrading from Kallithea 0.3.x or
146 older. That's expected. If you get an error that the config file was not
147 found or has no ``[alembic]`` section, see the next section.
148
149 Next, if you are performing an *upgrade*: Run the following command to
150 upgrade your database to the current Kallithea version::
151
152 alembic -c new.ini upgrade head
153
154 If you are performing a *downgrade*: Run the following command to
155 downgrade your database to the given version::
156
157 alembic -c new.ini downgrade 0.4
158
159 Alembic will show the necessary migrations (if any) as it executes them.
160 If no "ERROR" is displayed, the command was successful.
161
162 Should an error occur, the database may be "stranded" half-way
163 through the migration, and you should restore it from backup.
164
165 Enabling old Kallithea config files for Alembic use
166 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
167
168 Kallithea configuration files created before the introduction of Alembic
169 (i.e. predating Kallithea 0.4) need to be updated for use with Alembic.
170 Without this, Alembic will fail with an error like this::
171
172 FAILED: No config file 'my.ini' found, or file has no '[alembic]' section
173
174 .. note::
175 If you followed this upgrade guide correctly, you will have created a
176 new configuration file in section :ref:`Upgrading your configuration
177 <upgrade_config>`. When calling Alembic, make
178 sure to use this new config file. In this case, you should not get any
179 errors and the below manual steps should not be needed.
180
181 If Alembic complains specifically about a missing ``alembic.ini``, it is
182 likely because you did not specify a config file using the ``-c`` option.
183 On the other hand, if the mentioned config file actually exists, you
184 need to append the following lines to it::
185
186 [alembic]
187 script_location = kallithea:alembic
188
189 Your config file should now work with Alembic.
190
191
192 7. Prepare the front-end
193 ------------------------
194
195 Starting with Kallithea 0.4, external front-end dependencies are no longer
196 shipped but need to be downloaded and/or generated at installation time. Run the
197 following command::
198
199 kallithea-cli front-end-build
200
201
202 8. Rebuild the Whoosh full-text index
203 -------------------------------------
204
205 It is recommended that you rebuild the Whoosh index after upgrading since
206 new Whoosh versions can introduce incompatible index changes.
207
208
209 9. Start the Kallithea web application
210 --------------------------------------
211
212 This step once again depends entirely on the web server software used to
213 serve Kallithea.
214
215 If you were running Kallithea 0.3.x or older and were using ``paster serve
216 my.ini`` before, then the corresponding command in Kallithea 0.4 and later is::
217
218 gearbox serve -c new.ini
219
220 Before starting the new version of Kallithea, you may find it helpful to
221 clear out your log file so that new errors are readily apparent.
222
223 .. note::
224 If you're using Celery, make sure you restart all instances of it after
225 upgrade.
226
227
228 10. Update Git repository hooks
229 -------------------------------
230
231 It is possible that an upgrade involves changes to the Git hooks installed by
232 Kallithea. As these hooks are created inside the repositories on the server
233 filesystem, they are not updated automatically when upgrading Kallithea itself.
234
235 To update the hooks of your Git repositories:
236
237 * Go to *Admin > Settings > Remap and Rescan*
238 * Select the checkbox *Install Git hooks*
239 * Click the button *Rescan repositories*
240
241 .. note::
242 Kallithea does not use hooks on Mercurial repositories. This step is thus
243 not necessary if you only have Mercurial repositories.
244
245
246 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
@@ -0,0 +1,70 b''
1 .. _customization:
2
3 =============
4 Customization
5 =============
6
7 There are several ways to customize Kallithea to your needs depending on what
8 you want to achieve.
9
10
11 HTML/JavaScript/CSS customization
12 ---------------------------------
13
14 To customize the look-and-feel of the web interface (for example to add a
15 company banner or some JavaScript widget or to tweak the CSS style definitions)
16 you can enter HTML code (possibly with JavaScript and/or CSS) directly via the
17 *Admin > Settings > Global > HTML/JavaScript customization
18 block*.
19
20
21 Style sheet customization with Less
22 -----------------------------------
23
24 Kallithea uses `Bootstrap 3`_ and Less_ for its style definitions. If you want
25 to make some customizations, we recommend to do so by creating a ``theme.less``
26 file. When you create a file named ``theme.less`` in the Kallithea root
27 directory, you can use this file to override the default style. For example,
28 you can use this to override ``@kallithea-theme-main-color``,
29 ``@kallithea-logo-url`` or other `Bootstrap variables`_.
30
31 After creating the ``theme.less`` file, you need to regenerate the CSS files, by
32 running::
33
34 kallithea-cli front-end-build --no-install-deps
35
36 .. _bootstrap 3: https://getbootstrap.com/docs/3.3/
37 .. _bootstrap variables: https://getbootstrap.com/docs/3.3/customize/#less-variables
38 .. _less: http://lesscss.org/
39
40
41 Behavioral customization: rcextensions
42 --------------------------------------
43
44 Some behavioral customization can be done in Python using ``rcextensions``, a
45 custom Python package that can extend Kallithea functionality.
46
47 With ``rcextensions`` it's possible to add additional mappings for Whoosh
48 indexing and statistics, to add additional code into the push/pull/create/delete
49 repository hooks (for example to send signals to build bots such as Jenkins) and
50 even to monkey-patch certain parts of the Kallithea source code (for example
51 overwrite an entire function, change a global variable, ...).
52
53 To generate a skeleton extensions package, run::
54
55 kallithea-cli extensions-create -c my.ini
56
57 This will create an ``rcextensions`` package next to the specified ``ini`` file.
58 See the ``__init__.py`` file inside the generated ``rcextensions`` package
59 for more details.
60
61
62 Behavioral customization: code changes
63 --------------------------------------
64
65 As Kallithea is open-source software, you can make any changes you like directly
66 in the source code.
67
68 We encourage you to send generic improvements back to the
69 community so that Kallithea can become better. See :ref:`contributing` for more
70 details.
@@ -0,0 +1,105 b''
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15 # Alembic migration environment (configuration).
16
17 import logging
18 from logging.config import fileConfig
19
20 from alembic import context
21 from sqlalchemy import engine_from_config, pool
22
23 from kallithea.model import db
24
25
26 # The alembic.config.Config object, which wraps the current .ini file.
27 config = context.config
28
29 # Default to use the main Kallithea database string in [app:main].
30 # For advanced uses, this can be overridden by specifying an explicit
31 # [alembic] sqlalchemy.url.
32 database_url = (
33 config.get_main_option('sqlalchemy.url') or
34 config.get_section_option('app:main', 'sqlalchemy.url')
35 )
36
37 # Configure default logging for Alembic. (This can be overriden by the
38 # config file, but usually isn't.)
39 logging.getLogger('alembic').setLevel(logging.INFO)
40
41 # Setup Python loggers based on the config file provided to the alembic
42 # command. If we're being invoked via the Alembic API (presumably for
43 # stamping during "kallithea-cli db-create"), config_file_name is not available,
44 # and loggers are assumed to already have been configured.
45 if config.config_file_name:
46 fileConfig(config.config_file_name, disable_existing_loggers=False)
47
48
49 def include_in_autogeneration(object, name, type, reflected, compare_to):
50 """Filter changes subject to autogeneration of migrations. """
51
52 # Don't include changes to sqlite_sequence.
53 if type == 'table' and name == 'sqlite_sequence':
54 return False
55
56 return True
57
58
59 def run_migrations_offline():
60 """Run migrations in 'offline' (--sql) mode.
61
62 This produces an SQL script instead of directly applying the changes.
63 Some migrations may not run in offline mode.
64 """
65 context.configure(
66 url=database_url,
67 literal_binds=True,
68 )
69
70 with context.begin_transaction():
71 context.run_migrations()
72
73
74 def run_migrations_online():
75 """Run migrations in 'online' mode.
76
77 Connects to the database and directly applies the necessary
78 migrations.
79 """
80 cfg = config.get_section(config.config_ini_section)
81 cfg['sqlalchemy.url'] = database_url
82 connectable = engine_from_config(
83 cfg,
84 prefix='sqlalchemy.',
85 poolclass=pool.NullPool)
86
87 with connectable.connect() as connection:
88 context.configure(
89 connection=connection,
90
91 # Support autogeneration of migration scripts based on "diff" between
92 # current database schema and kallithea.model.db schema.
93 target_metadata=db.Base.metadata,
94 include_object=include_in_autogeneration,
95 render_as_batch=True, # batch mode is needed for SQLite support
96 )
97
98 with context.begin_transaction():
99 context.run_migrations()
100
101
102 if context.is_offline_mode():
103 run_migrations_offline()
104 else:
105 run_migrations_online()
@@ -0,0 +1,40 b''
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15 ## Template for creating new Alembic migration scripts.
16 """${message}
17
18 Revision ID: ${up_revision}
19 Revises: ${down_revision | comma,n}
20 Create Date: ${create_date}
21
22 """
23
24 # The following opaque hexadecimal identifiers ("revisions") are used
25 # by Alembic to track this migration script and its relations to others.
26 revision = ${repr(up_revision)}
27 down_revision = ${repr(down_revision)}
28 branch_labels = ${repr(branch_labels)}
29 depends_on = ${repr(depends_on)}
30
31 from alembic import op
32 import sqlalchemy as sa
33 ${imports if imports else ""}
34
35 def upgrade():
36 ${upgrades if upgrades else "pass"}
37
38
39 def downgrade():
40 ${downgrades if downgrades else "pass"}
@@ -0,0 +1,37 b''
1 # This program is free software: you can redistribute it and/or modify
2 # it under the terms of the GNU General Public License as published by
3 # the Free Software Foundation, either version 3 of the License, or
4 # (at your option) any later version.
5 #
6 # This program is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # GNU General Public License for more details.
10 #
11 # You should have received a copy of the GNU General Public License
12 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13
14 """Drop SQLAlchemy Migrate support
15
16 Revision ID: 9358dc3d6828
17 Revises:
18 Create Date: 2016-03-01 15:21:30.896585
19
20 """
21
22 # The following opaque hexadecimal identifiers ("revisions") are used
23 # by Alembic to track this migration script and its relations to others.
24 revision = '9358dc3d6828'
25 down_revision = None
26 branch_labels = None
27 depends_on = None
28
29 from alembic import op
30
31
32 def upgrade():
33 op.drop_table('db_migrate_version')
34
35
36 def downgrade():
37 raise NotImplementedError('cannot revert to SQLAlchemy Migrate')
@@ -0,0 +1,61 b''
1 # This program is free software: you can redistribute it and/or modify
2 # it under the terms of the GNU General Public License as published by
3 # the Free Software Foundation, either version 3 of the License, or
4 # (at your option) any later version.
5 #
6 # This program is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # GNU General Public License for more details.
10 #
11 # You should have received a copy of the GNU General Public License
12 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13
14 """rename hooks
15
16 Revision ID: a020f7044fd6
17 Revises: 9358dc3d6828
18 Create Date: 2017-11-24 13:35:14.374000
19
20 """
21
22 # The following opaque hexadecimal identifiers ("revisions") are used
23 # by Alembic to track this migration script and its relations to others.
24 revision = 'a020f7044fd6'
25 down_revision = '9358dc3d6828'
26 branch_labels = None
27 depends_on = None
28
29 from alembic import op
30 from kallithea.model.db import Ui
31 from sqlalchemy import Table, MetaData
32
33 meta = MetaData()
34
35
36 def upgrade():
37 meta.bind = op.get_bind()
38 ui = Table(Ui.__tablename__, meta, autoload=True)
39
40 ui.update(values={
41 'ui_key': 'prechangegroup.push_lock_handling',
42 'ui_value': 'python:kallithea.lib.hooks.push_lock_handling',
43 }).where(ui.c.ui_key == 'prechangegroup.pre_push').execute()
44 ui.update(values={
45 'ui_key': 'preoutgoing.pull_lock_handling',
46 'ui_value': 'python:kallithea.lib.hooks.pull_lock_handling',
47 }).where(ui.c.ui_key == 'preoutgoing.pre_pull').execute()
48
49
50 def downgrade():
51 meta.bind = op.get_bind()
52 ui = Table(Ui.__tablename__, meta, autoload=True)
53
54 ui.update(values={
55 'ui_key': 'prechangegroup.pre_push',
56 'ui_value': 'python:kallithea.lib.hooks.pre_push',
57 }).where(ui.c.ui_key == 'prechangegroup.push_lock_handling').execute()
58 ui.update(values={
59 'ui_key': 'preoutgoing.pre_pull',
60 'ui_value': 'python:kallithea.lib.hooks.pre_pull',
61 }).where(ui.c.ui_key == 'preoutgoing.pull_lock_handling').execute()
@@ -0,0 +1,27 b''
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15 # 'cli' is the main entry point for 'kallithea-cli', specified in setup.py as entry_points console_scripts
16 from kallithea.bin.kallithea_cli_base import cli
17
18 # import commands (they will add themselves to the 'cli' object)
19 import kallithea.bin.kallithea_cli_celery
20 import kallithea.bin.kallithea_cli_config
21 import kallithea.bin.kallithea_cli_db
22 import kallithea.bin.kallithea_cli_extensions
23 import kallithea.bin.kallithea_cli_front_end
24 import kallithea.bin.kallithea_cli_iis
25 import kallithea.bin.kallithea_cli_index
26 import kallithea.bin.kallithea_cli_ishell
27 import kallithea.bin.kallithea_cli_repo
@@ -0,0 +1,55 b''
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15 import click
16 import functools
17 import os
18
19 import kallithea
20 import logging.config
21 import paste.deploy
22
23
24 # This placeholder is the main entry point for the kallithea-cli command
25 @click.group()
26 def cli():
27 """Various commands to manage a Kallithea instance."""
28
29 def register_command(config_file=False, config_file_initialize_app=False):
30 """Register a kallithea-cli subcommand.
31
32 If one of the config_file flags are true, a config file must be specified
33 with -c and it is read and logging is configured. The configuration is
34 available in the kallithea.CONFIG dict.
35
36 If config_file_initialize_app is true, Kallithea, TurboGears global state
37 (including tg.config), and database access will also be fully initialized.
38 """
39 cli_command = cli.command()
40 if config_file or config_file_initialize_app:
41 def annotator(annotated):
42 @click.option('--config_file', '-c', help="Path to .ini file with app configuration.",
43 type=click.Path(dir_okay=False, exists=True, readable=True), required=True)
44 @functools.wraps(annotated) # reuse meta data from the wrapped function so click can see other options
45 def runtime_wrapper(config_file, *args, **kwargs):
46 path_to_ini_file = os.path.realpath(config_file)
47 kallithea.CONFIG = paste.deploy.appconfig('config:' + path_to_ini_file)
48 logging.config.fileConfig(path_to_ini_file)
49 if config_file_initialize_app:
50 kallithea.config.middleware.make_app_without_logging(kallithea.CONFIG.global_conf, **kallithea.CONFIG.local_conf)
51 kallithea.lib.utils.setup_cache_regions(kallithea.CONFIG)
52 return annotated(*args, **kwargs)
53 return cli_command(runtime_wrapper)
54 return annotator
55 return cli_command
@@ -0,0 +1,108 b''
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15 import click
16 import kallithea.bin.kallithea_cli_base as cli_base
17
18 import os
19 import shutil
20 import subprocess
21 import json
22
23 import kallithea
24
25 @cli_base.register_command()
26 @click.option('--install-deps/--no-install-deps', default=True,
27 help='Skip installation of dependencies, via "npm".')
28 @click.option('--generate/--no-generate', default=True,
29 help='Skip generation of front-end files.')
30 def front_end_build(install_deps, generate):
31 """Build the front-end.
32
33 Install required dependencies for the front-end and generate the necessary
34 files. This step is complementary to any 'pip install' step which only
35 covers Python dependencies.
36
37 The installation of front-end dependencies happens via the tool 'npm' which
38 is expected to be installed already.
39 """
40 front_end_dir = os.path.abspath(os.path.join(kallithea.__file__, '..', 'front-end'))
41 public_dir = os.path.abspath(os.path.join(kallithea.__file__, '..', 'public'))
42
43 if install_deps:
44 click.echo("Running 'npm install' to install front-end dependencies from package.json")
45 subprocess.check_call(['npm', 'install'], cwd=front_end_dir, shell=kallithea.is_windows)
46
47 if generate:
48 tmp_dir = os.path.join(front_end_dir, 'tmp')
49 if not os.path.isdir(tmp_dir):
50 os.mkdir(tmp_dir)
51
52 click.echo("Building CSS styling based on Bootstrap")
53 with open(os.path.join(tmp_dir, 'pygments.css'), 'w') as f:
54 subprocess.check_call(['pygmentize',
55 '-S', 'default',
56 '-f', 'html',
57 '-a', '.code-highlight'],
58 stdout=f)
59 lesscpath = os.path.join(front_end_dir, 'node_modules', '.bin', 'lessc')
60 lesspath = os.path.join(front_end_dir, 'main.less')
61 csspath = os.path.join(public_dir, 'css', 'style.css')
62 subprocess.check_call([lesscpath, '--source-map',
63 '--source-map-less-inline', lesspath, csspath],
64 cwd=front_end_dir, shell=kallithea.is_windows)
65
66 click.echo("Preparing Bootstrap JS")
67 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'bootstrap', 'dist', 'js', 'bootstrap.js'), os.path.join(public_dir, 'js', 'bootstrap.js'))
68
69 click.echo("Preparing jQuery JS with Flot, Caret and Atwho")
70 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'jquery', 'dist', 'jquery.min.js'), os.path.join(public_dir, 'js', 'jquery.min.js'))
71 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'jquery.flot', 'jquery.flot.js'), os.path.join(public_dir, 'js', 'jquery.flot.js'))
72 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'jquery.flot', 'jquery.flot.selection.js'), os.path.join(public_dir, 'js', 'jquery.flot.selection.js'))
73 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'jquery.flot', 'jquery.flot.time.js'), os.path.join(public_dir, 'js', 'jquery.flot.time.js'))
74 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'jquery.caret', 'dist', 'jquery.caret.min.js'), os.path.join(public_dir, 'js', 'jquery.caret.min.js'))
75 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'at.js', 'dist', 'js', 'jquery.atwho.min.js'), os.path.join(public_dir, 'js', 'jquery.atwho.min.js'))
76
77 click.echo("Preparing DataTables JS")
78 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'datatables.net', 'js', 'jquery.dataTables.js'), os.path.join(public_dir, 'js', 'jquery.dataTables.js'))
79 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'datatables.net-bs', 'js', 'dataTables.bootstrap.js'), os.path.join(public_dir, 'js', 'dataTables.bootstrap.js'))
80
81 click.echo("Preparing Select2 JS")
82 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'select2', 'select2.js'), os.path.join(public_dir, 'js', 'select2.js'))
83 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'select2', 'select2.png'), os.path.join(public_dir, 'css', 'select2.png'))
84 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'select2', 'select2x2.png'), os.path.join(public_dir, 'css', 'select2x2.png'))
85 shutil.copy(os.path.join(front_end_dir, 'node_modules', 'select2', 'select2-spinner.gif'), os.path.join(public_dir, 'css', 'select2-spinner.gif'))
86
87 click.echo("Preparing CodeMirror JS")
88 if os.path.isdir(os.path.join(public_dir, 'codemirror')):
89 shutil.rmtree(os.path.join(public_dir, 'codemirror'))
90 shutil.copytree(os.path.join(front_end_dir, 'node_modules', 'codemirror'), os.path.join(public_dir, 'codemirror'))
91
92 click.echo("Generating LICENSES.txt")
93 license_checker_path = os.path.join(front_end_dir, 'node_modules', '.bin', 'license-checker')
94 check_licensing_json_path = os.path.join(tmp_dir, 'licensing.json')
95 licensing_txt_path = os.path.join(public_dir, 'LICENSES.txt')
96 subprocess.check_call([license_checker_path, '--json', '--out', check_licensing_json_path],
97 cwd=front_end_dir, shell=kallithea.is_windows)
98 with open(check_licensing_json_path) as jsonfile:
99 rows = json.loads(jsonfile.read())
100 with open(licensing_txt_path, 'w') as out:
101 out.write("The Kallithea front-end was built using the following Node modules:\n\n")
102 for name_version, values in sorted(rows.items()):
103 name, version = name_version.rsplit('@', 1)
104 line = "%s from https://www.npmjs.com/package/%s/v/%s\n License: %s\n Repository: %s\n" % (
105 name_version, name, version, values['licenses'], values.get('repository', '-'))
106 if values.get('copyright'):
107 line += " Copyright: %s\n" % (values['copyright'])
108 out.write(line + '\n')
@@ -0,0 +1,185 b''
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
15 This file was forked by the Kallithea project in July 2014 and later moved.
16 Original author and date, and relevant copyright and licensing information is below:
17 :created_on: Feb 9, 2013
18 :author: marcink
19 :copyright: (c) 2013 RhodeCode GmbH, and others.
20 :license: GPLv3, see LICENSE.md for more details.
21 """
22 import click
23 import kallithea.bin.kallithea_cli_base as cli_base
24
25 import datetime
26 import os
27 import re
28 import shutil
29
30 from kallithea.lib.utils import repo2db_mapper, REMOVED_REPO_PAT
31 from kallithea.lib.utils2 import safe_unicode, safe_str, ask_ok
32 from kallithea.model.db import Repository, Ui
33 from kallithea.model.meta import Session
34 from kallithea.model.scm import ScmModel
35
36 @cli_base.register_command(config_file_initialize_app=True)
37 @click.option('--remove-missing', is_flag=True,
38 help='Remove missing repositories from the Kallithea database.')
39 def repo_scan(remove_missing):
40 """Scan filesystem for repositories.
41
42 Search the configured repository root for new repositories and add them
43 into Kallithea.
44 Additionally, report repositories that were previously known to Kallithea
45 but are no longer present on the filesystem. If option --remove-missing is
46 given, remove the missing repositories from the Kallithea database.
47 """
48 click.echo('Now scanning root location for new repos ...')
49 added, removed = repo2db_mapper(ScmModel().repo_scan(),
50 remove_obsolete=remove_missing)
51 click.echo('Scan completed.')
52 if added:
53 click.echo('Added: %s' % ', '.join(added))
54 if removed:
55 click.echo('%s: %s' % ('Removed' if remove_missing else 'Missing',
56 ', '.join(removed)))
57
58 @cli_base.register_command(config_file_initialize_app=True)
59 @click.argument('repositories', nargs=-1)
60 def repo_update_metadata(repositories):
61 """
62 Update repository metadata in database from repository content.
63
64 In normal operation, Kallithea will keep caches up-to-date
65 automatically. However, if repositories are externally modified, e.g. by
66 a direct push via the filesystem rather than via a Kallithea URL,
67 Kallithea is not aware of it. In this case, you should manually run this
68 command to update the repository cache.
69
70 If no repositories are specified, the caches of all repositories are
71 updated.
72 """
73 if not repositories:
74 repo_list = Repository.query().all()
75 else:
76 repo_names = [safe_unicode(n.strip()) for n in repositories]
77 repo_list = list(Repository.query()
78 .filter(Repository.repo_name.in_(repo_names)))
79
80 for repo in repo_list:
81 # update latest revision metadata in database
82 repo.update_changeset_cache()
83 # invalidate in-memory VCS object cache... will be repopulated on
84 # first access
85 repo.set_invalidate()
86
87 Session().commit()
88
89 click.echo('Updated database with information about latest change in the following %s repositories:' % (len(repo_list)))
90 click.echo('\n'.join(repo.repo_name for repo in repo_list))
91
92 @cli_base.register_command(config_file_initialize_app=True)
93 @click.option('--ask/--no-ask', default=True, help='Ask for confirmation or not. Default is --ask.')
94 @click.option('--older-than',
95 help="""Only purge repositories that have been removed at least the given time ago.
96 For example, '--older-than=30d' purges repositories deleted 30 days ago or longer.
97 Possible suffixes: d (days), h (hours), m (minutes), s (seconds).""")
98 def repo_purge_deleted(ask, older_than):
99 """Purge backups of deleted repositories.
100
101 When a repository is deleted via the Kallithea web interface, the actual
102 data is still present on the filesystem but set aside using a special name.
103 This command allows to delete these files permanently.
104 """
105 def _parse_older_than(val):
106 regex = re.compile(r'((?P<days>\d+?)d)?((?P<hours>\d+?)h)?((?P<minutes>\d+?)m)?((?P<seconds>\d+?)s)?')
107 parts = regex.match(val)
108 if not parts:
109 return
110 parts = parts.groupdict()
111 time_params = {}
112 for (name, param) in parts.iteritems():
113 if param:
114 time_params[name] = int(param)
115 return datetime.timedelta(**time_params)
116
117 def _extract_date(name):
118 """
119 Extract the date part from rm__<date> pattern of removed repos,
120 and convert it to datetime object
121
122 :param name:
123 """
124 date_part = name[4:19] # 4:19 since we don't parse milliseconds
125 return datetime.datetime.strptime(date_part, '%Y%m%d_%H%M%S')
126
127 repos_location = Ui.get_repos_location()
128 to_remove = []
129 for dn_, dirs, f in os.walk(safe_str(repos_location)):
130 alldirs = list(dirs)
131 del dirs[:]
132 if ('.hg' in alldirs or
133 '.git' in alldirs or
134 '.svn' in alldirs or
135 'objects' in alldirs and ('refs' in alldirs or 'packed-refs' in f)):
136 continue
137 for loc in alldirs:
138 if REMOVED_REPO_PAT.match(loc):
139 to_remove.append([os.path.join(dn_, loc),
140 _extract_date(loc)])
141 else:
142 dirs.append(loc)
143 if dirs:
144 click.echo('Scanning: %s' % dn_)
145
146 if not to_remove:
147 click.echo('There are no deleted repositories.')
148 return
149
150 # filter older than (if present)!
151 if older_than:
152 now = datetime.datetime.now()
153 to_remove_filtered = []
154 older_than_date = _parse_older_than(older_than)
155 for name, date_ in to_remove:
156 repo_age = now - date_
157 if repo_age > older_than_date:
158 to_remove_filtered.append([name, date_])
159
160 to_remove = to_remove_filtered
161
162 if not to_remove:
163 click.echo('There are no deleted repositories older than %s (%s)'
164 % (older_than, older_than_date))
165 return
166
167 click.echo('Considering %s deleted repositories older than %s (%s).'
168 % (len(to_remove), older_than, older_than_date))
169 else:
170 click.echo('Considering %s deleted repositories.' % len(to_remove))
171
172 if not ask:
173 remove = True
174 else:
175 remove = ask_ok('The following repositories will be removed completely:\n%s\n'
176 'Do you want to proceed? [y/n] '
177 % '\n'.join(['%s deleted on %s' % (safe_str(x[0]), safe_str(x[1]))
178 for x in to_remove]))
179
180 if remove:
181 for path, date_ in to_remove:
182 click.echo('Purging repository %s' % path)
183 shutil.rmtree(path)
184 else:
185 click.echo('Nothing done, exiting...')
@@ -0,0 +1,214 b''
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
15 Global configuration file for TurboGears2 specific settings in Kallithea.
16
17 This file complements the .ini file.
18 """
19
20 import platform
21 import os, sys, logging
22
23 import tg
24 from tg import hooks
25 from tg.configuration import AppConfig
26 from tg.support.converters import asbool
27 import alembic.config
28 from alembic.script.base import ScriptDirectory
29 from alembic.migration import MigrationContext
30 from sqlalchemy import create_engine
31 import mercurial
32
33 from kallithea.lib.middleware.https_fixup import HttpsFixup
34 from kallithea.lib.middleware.simplegit import SimpleGit
35 from kallithea.lib.middleware.simplehg import SimpleHg
36 from kallithea.lib.auth import set_available_permissions
37 from kallithea.lib.utils import load_rcextensions, make_ui, set_app_settings, set_vcs_config, \
38 set_indexer_config, check_git_version, repo2db_mapper
39 from kallithea.lib.utils2 import str2bool
40 import kallithea.model.base
41 from kallithea.model.scm import ScmModel
42
43 import formencode
44
45 log = logging.getLogger(__name__)
46
47
48 class KallitheaAppConfig(AppConfig):
49 # Note: AppConfig has a misleading name, as it's not the application
50 # configuration, but the application configurator. The AppConfig values are
51 # used as a template to create the actual configuration, which might
52 # overwrite or extend the one provided by the configurator template.
53
54 # To make it clear, AppConfig creates the config and sets into it the same
55 # values that AppConfig itself has. Then the values from the config file and
56 # gearbox options are loaded and merged into the configuration. Then an
57 # after_init_config(conf) method of AppConfig is called for any change that
58 # might depend on options provided by configuration files.
59
60 def __init__(self):
61 super(KallitheaAppConfig, self).__init__()
62
63 self['package'] = kallithea
64
65 self['prefer_toscawidgets2'] = False
66 self['use_toscawidgets'] = False
67
68 self['renderers'] = []
69
70 # Enable json in expose
71 self['renderers'].append('json')
72
73 # Configure template rendering
74 self['renderers'].append('mako')
75 self['default_renderer'] = 'mako'
76 self['use_dotted_templatenames'] = False
77
78 # Configure Sessions, store data as JSON to avoid pickle security issues
79 self['session.enabled'] = True
80 self['session.data_serializer'] = 'json'
81
82 # Configure the base SQLALchemy Setup
83 self['use_sqlalchemy'] = True
84 self['model'] = kallithea.model.base
85 self['DBSession'] = kallithea.model.meta.Session
86
87 # Configure App without an authentication backend.
88 self['auth_backend'] = None
89
90 # Use custom error page for these errors. By default, Turbogears2 does not add
91 # 400 in this list.
92 # Explicitly listing all is considered more robust than appending to defaults,
93 # in light of possible future framework changes.
94 self['errorpage.status_codes'] = [400, 401, 403, 404]
95
96 # Disable transaction manager -- currently Kallithea takes care of transactions itself
97 self['tm.enabled'] = False
98
99
100 base_config = KallitheaAppConfig()
101
102 # TODO still needed as long as we use pylonslib
103 sys.modules['pylons'] = tg
104
105 # DebugBar, a debug toolbar for TurboGears2.
106 # (https://github.com/TurboGears/tgext.debugbar)
107 # To enable it, install 'tgext.debugbar' and 'kajiki', and run Kallithea with
108 # 'debug = true' (not in production!)
109 # See the Kallithea documentation for more information.
110 try:
111 from tgext.debugbar import enable_debugbar
112 import kajiki # only to check its existence
113 except ImportError:
114 pass
115 else:
116 base_config['renderers'].append('kajiki')
117 enable_debugbar(base_config)
118
119
120 def setup_configuration(app):
121 config = app.config
122
123 # Verify that things work when Dulwich passes unicode paths to the file system layer.
124 # Note: UTF-8 is preferred, but for example ISO-8859-1 or mbcs should also work under the right cirumstances.
125 try:
126 u'\xe9'.encode(sys.getfilesystemencoding()) # Test using é (&eacute;)
127 except UnicodeEncodeError:
128 log.error("Cannot encode Unicode paths to file system encoding %r", sys.getfilesystemencoding())
129 for var in ['LC_CTYPE', 'LC_ALL', 'LANG']:
130 if var in os.environ:
131 val = os.environ[var]
132 log.error("Note: Environment variable %s is %r - perhaps change it to some other value from 'locale -a', like 'C.UTF-8' or 'en_US.UTF-8'", var, val)
133 break
134 else:
135 log.error("Note: No locale setting found in environment variables - perhaps set LC_CTYPE to some value from 'locale -a', like 'C.UTF-8' or 'en_US.UTF-8'")
136 log.error("Terminating ...")
137 sys.exit(1)
138
139 # Mercurial sets encoding at module import time, so we have to monkey patch it
140 hgencoding = config.get('hgencoding')
141 if hgencoding:
142 mercurial.encoding.encoding = hgencoding
143
144 if config.get('ignore_alembic_revision', False):
145 log.warn('database alembic revision checking is disabled')
146 else:
147 dbconf = config['sqlalchemy.url']
148 alembic_cfg = alembic.config.Config()
149 alembic_cfg.set_main_option('script_location', 'kallithea:alembic')
150 alembic_cfg.set_main_option('sqlalchemy.url', dbconf)
151 script_dir = ScriptDirectory.from_config(alembic_cfg)
152 available_heads = sorted(script_dir.get_heads())
153
154 engine = create_engine(dbconf)
155 with engine.connect() as conn:
156 context = MigrationContext.configure(conn)
157 current_heads = sorted(str(s) for s in context.get_current_heads())
158 if current_heads != available_heads:
159 log.error('Failed to run Kallithea:\n\n'
160 'The database version does not match the Kallithea version.\n'
161 'Please read the documentation on how to upgrade or downgrade the database.\n'
162 'Current database version id(s): %s\n'
163 'Expected database version id(s): %s\n'
164 'If you are a developer and you know what you are doing, you can add `ignore_alembic_revision = True` '
165 'to your .ini file to skip the check.\n' % (' '.join(current_heads), ' '.join(available_heads)))
166 sys.exit(1)
167
168 # store some globals into kallithea
169 kallithea.CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
170 kallithea.CELERY_EAGER = str2bool(config['app_conf'].get('celery.always.eager'))
171 kallithea.CONFIG = config
172
173 load_rcextensions(root_path=config['here'])
174
175 set_available_permissions(config)
176 repos_path = make_ui('db').configitems('paths')[0][1]
177 config['base_path'] = repos_path
178 set_app_settings(config)
179
180 instance_id = kallithea.CONFIG.get('instance_id', '*')
181 if instance_id == '*':
182 instance_id = '%s-%s' % (platform.uname()[1], os.getpid())
183 kallithea.CONFIG['instance_id'] = instance_id
184
185 # update kallithea.CONFIG with the meanwhile changed 'config'
186 kallithea.CONFIG.update(config)
187
188 # configure vcs and indexer libraries (they are supposed to be independent
189 # as much as possible and thus avoid importing tg.config or
190 # kallithea.CONFIG).
191 set_vcs_config(kallithea.CONFIG)
192 set_indexer_config(kallithea.CONFIG)
193
194 check_git_version()
195
196
197 hooks.register('configure_new_app', setup_configuration)
198
199
200 def setup_application(app):
201 config = app.config
202
203 # we want our low level middleware to get to the request ASAP. We don't
204 # need any stack middleware in them - especially no StatusCodeRedirect buffering
205 app = SimpleHg(app, config)
206 app = SimpleGit(app, config)
207
208 # Enable https redirects based on HTTP_X_URL_SCHEME set by proxy
209 if any(asbool(config.get(x)) for x in ['https_fixup', 'force_https', 'use_htsts']):
210 app = HttpsFixup(app, config)
211 return app
212
213
214 hooks.register('before_config', setup_application)
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
@@ -16,12 +16,31 b' syntax: regexp'
16 ^docs/build/
16 ^docs/build/
17 ^docs/_build/
17 ^docs/_build/
18 ^data$
18 ^data$
19 ^kallithea/tests/data$
20 ^sql_dumps/
19 ^sql_dumps/
21 ^\.settings$
20 ^\.settings$
22 ^\.project$
21 ^\.project$
23 ^\.pydevproject$
22 ^\.pydevproject$
24 ^\.coverage$
23 ^\.coverage$
24 ^kallithea/front-end/node_modules$
25 ^kallithea/front-end/package-lock\.json$
26 ^kallithea/front-end/tmp$
27 ^kallithea/public/codemirror$
28 ^kallithea/public/css/select2-spinner\.gif$
29 ^kallithea/public/css/select2\.png$
30 ^kallithea/public/css/select2x2\.png$
31 ^kallithea/public/css/style\.css$
32 ^kallithea/public/css/style\.css\.map$
33 ^kallithea/public/js/bootstrap\.js$
34 ^kallithea/public/js/dataTables\.bootstrap\.js$
35 ^kallithea/public/js/jquery\.atwho\.min\.js$
36 ^kallithea/public/js/jquery\.caret\.min\.js$
37 ^kallithea/public/js/jquery\.dataTables\.js$
38 ^kallithea/public/js/jquery\.flot\.js$
39 ^kallithea/public/js/jquery\.flot\.selection\.js$
40 ^kallithea/public/js/jquery\.flot\.time\.js$
41 ^kallithea/public/js/jquery\.min\.js$
42 ^kallithea/public/js/select2\.js$
43 ^theme\.less$
25 ^kallithea\.db$
44 ^kallithea\.db$
26 ^test\.db$
45 ^test\.db$
27 ^Kallithea\.egg-info$
46 ^Kallithea\.egg-info$
@@ -29,3 +48,5 b' syntax: regexp'
29 ^fabfile.py
48 ^fabfile.py
30 ^\.idea$
49 ^\.idea$
31 ^\.cache$
50 ^\.cache$
51 ^\.pytest_cache$
52 /__pycache__$
@@ -69,3 +69,5 b' cf635c823ea059cc3a1581b82d8672e46b682384'
69 4cca4cc6a0a97f4c4763317184cd41aca4297630 0.3.5
69 4cca4cc6a0a97f4c4763317184cd41aca4297630 0.3.5
70 082c9b8f0f17bd34740eb90c69bdc4c80d4b5b31 0.3.6
70 082c9b8f0f17bd34740eb90c69bdc4c80d4b5b31 0.3.6
71 a18445b85d407294da0b7f1d8be3bedef5ffdea6 0.3.7
71 a18445b85d407294da0b7f1d8be3bedef5ffdea6 0.3.7
72 8db761c407685e7b08b800c947890035b0d67025 0.4.0rc1
73 60f726162fd6c515bd819feb423be73cad01d7d3 0.4.0rc2
@@ -1,28 +1,51 b''
1 List of contributors to Kallithea project:
1 List of contributors to Kallithea project:
2
2
3 Andrej Shadura <andrew@shadura.me> 2012 2014-2017 2019
3 Thomas De Schampheleire <thomas.de_schampheleire@nokia.com> 2014-2019
4 Thomas De Schampheleire <thomas.de_schampheleire@nokia.com> 2014-2019
5 Étienne Gilli <etienne.gilli@gmail.com> 2015-2017 2019
4 Mads Kiilerich <mads@kiilerich.com> 2016-2019
6 Mads Kiilerich <mads@kiilerich.com> 2016-2019
7 Allan Nordhøy <epost@anotheragency.no> 2017-2019
8 Danni Randeris <danniranderis@gmail.com> 2019
9 Edmund Wong <ewong@crazy-cat.org> 2019
10 Manuel Jacob <me@manueljacob.de> 2019
11 Dominik Ruf <dominikruf@gmail.com> 2012 2014-2018
5 Michal Čihař <michal@cihar.com> 2014-2015 2018
12 Michal Čihař <michal@cihar.com> 2014-2015 2018
6 Branko Majic <branko@majic.rs> 2015 2018
13 Branko Majic <branko@majic.rs> 2015 2018
14 Chris Rule <crule@aegistg.com> 2018
15 Jesús Sánchez <jsanchezfdz95@gmail.com> 2018
16 Patrick Vane <patrick_vane@lowentry.com> 2018
17 Pheng Heong Tan <phtan90@gmail.com> 2018
7 ssantos <ssantos@web.de> 2018
18 ssantos <ssantos@web.de> 2018
8 Максим Якимчук <xpinovo@gmail.com> 2018
19 Максим Якимчук <xpinovo@gmail.com> 2018
9 Марс Ямбар <mjambarmeta@gmail.com> 2018
20 Марс Ямбар <mjambarmeta@gmail.com> 2018
10 Mads Kiilerich <madski@unity3d.com> 2012-2017
21 Mads Kiilerich <madski@unity3d.com> 2012-2017
11 Unity Technologies 2012-2017
22 Unity Technologies 2012-2017
12 Andrew Shadura <andrew@shadura.me> 2012 2014-2017
23 Søren Løvborg <sorenl@unity3d.com> 2015-2017
13 Dominik Ruf <dominikruf@gmail.com> 2012 2014 2016-2017
14 Étienne Gilli <etienne.gilli@gmail.com> 2015 2017
15 Sam Jaques <sam.jaques@me.com> 2015 2017
24 Sam Jaques <sam.jaques@me.com> 2015 2017
16 Allan Nordhøy <epost@anotheragency.no> 2017
25 Asterios Dimitriou <steve@pci.gr> 2016-2017
26 Alessandro Molina <alessandro.molina@axant.it> 2017
27 Anton Schur <tonich.sh@gmail.com> 2017
17 Ching-Chen Mao <mao@lins.fju.edu.tw> 2017
28 Ching-Chen Mao <mao@lins.fju.edu.tw> 2017
29 Eivind Tagseth <eivindt@gmail.com> 2017
18 FUJIWARA Katsunori <foozy@lares.dti.ne.jp> 2017
30 FUJIWARA Katsunori <foozy@lares.dti.ne.jp> 2017
31 Holger Schramm <info@schramm.by> 2017
32 Karl Goetz <karl@kgoetz.id.au> 2017
33 Lars Kruse <devel@sumpfralle.de> 2017
34 Marko Semet <markosemet@googlemail.com> 2017
19 Viktar Vauchkevich <victorenator@gmail.com> 2017
35 Viktar Vauchkevich <victorenator@gmail.com> 2017
20 Takumi IINO <trot.thunder@gmail.com> 2012-2016
36 Takumi IINO <trot.thunder@gmail.com> 2012-2016
21 Søren Løvborg <sorenl@unity3d.com> 2015-2016
37 Jan Heylen <heyleke@gmail.com> 2015-2016
38 Robert Martinez <ntttq@inboxen.org> 2015-2016
39 Robert Rauch <mail@robertrauch.de> 2015-2016
40 Angel Ezquerra <angel.ezquerra@gmail.com> 2016
22 Anton Shestakov <av6@dwimlabs.net> 2016
41 Anton Shestakov <av6@dwimlabs.net> 2016
23 Brandon Jones <bjones14@gmail.com> 2016
42 Brandon Jones <bjones14@gmail.com> 2016
43 Kateryna Musina <kateryna@unity3d.com> 2016
24 Konstantin Veretennicov <kveretennicov@gmail.com> 2016
44 Konstantin Veretennicov <kveretennicov@gmail.com> 2016
45 Oscar Curero <oscar@naiandei.net> 2016
25 Robert James Dennington <tinytimrob@googlemail.com> 2016
46 Robert James Dennington <tinytimrob@googlemail.com> 2016
47 timeless@gmail.com 2016
48 YFdyh000 <yfdyh000@gmail.com> 2016
26 Aras Pranckevičius <aras@unity3d.com> 2012-2013 2015
49 Aras Pranckevičius <aras@unity3d.com> 2012-2013 2015
27 Sean Farley <sean.michael.farley@gmail.com> 2013-2015
50 Sean Farley <sean.michael.farley@gmail.com> 2013-2015
28 Christian Oyarzun <oyarzun@gmail.com> 2014-2015
51 Christian Oyarzun <oyarzun@gmail.com> 2014-2015
@@ -37,7 +60,7 b' List of contributors to Kallithea projec'
37 duanhongyi <duanhongyi@doopai.com> 2015
60 duanhongyi <duanhongyi@doopai.com> 2015
38 EriCSN Chang <ericsning@gmail.com> 2015
61 EriCSN Chang <ericsning@gmail.com> 2015
39 Grzegorz Krason <grzegorz.krason@gmail.com> 2015
62 Grzegorz Krason <grzegorz.krason@gmail.com> 2015
40 Jan Heylen <heyleke@gmail.com> 2015
63 Jiří Suchan <yed@vanyli.net> 2015
41 Kazunari Kobayashi <kobanari@nifty.com> 2015
64 Kazunari Kobayashi <kobanari@nifty.com> 2015
42 Kevin Bullock <kbullock@ringworld.org> 2015
65 Kevin Bullock <kbullock@ringworld.org> 2015
43 kobanari <kobanari@nifty.com> 2015
66 kobanari <kobanari@nifty.com> 2015
@@ -50,11 +73,10 b' List of contributors to Kallithea projec'
50 Nick High <nick@silverchip.org> 2015
73 Nick High <nick@silverchip.org> 2015
51 Niemand Jedermann <predatorix@web.de> 2015
74 Niemand Jedermann <predatorix@web.de> 2015
52 Peter Vitt <petervitt@web.de> 2015
75 Peter Vitt <petervitt@web.de> 2015
53 Robert Martinez <ntttq@inboxen.org> 2015
54 Robert Rauch <mail@robertrauch.de> 2015
55 Ronny Pfannschmidt <opensource@ronnypfannschmidt.de> 2015
76 Ronny Pfannschmidt <opensource@ronnypfannschmidt.de> 2015
56 Tuux <tuxa@galaxie.eu.org> 2015
77 Tuux <tuxa@galaxie.eu.org> 2015
57 Viktar Palstsiuk <vipals@gmail.com> 2015
78 Viktar Palstsiuk <vipals@gmail.com> 2015
79 Ante Ilic <ante@unity3d.com> 2014
58 Bradley M. Kuhn <bkuhn@sfconservancy.org> 2014
80 Bradley M. Kuhn <bkuhn@sfconservancy.org> 2014
59 Calinou <calinou@opmbx.org> 2014
81 Calinou <calinou@opmbx.org> 2014
60 Daniel Anderson <daniel@dattrix.com> 2014
82 Daniel Anderson <daniel@dattrix.com> 2014
@@ -22,27 +22,39 b' Various third-party code under GPLv3-com'
22 of Kallithea.
22 of Kallithea.
23
23
24
24
25 Alembic
26 -------
27
28 Kallithea incorporates an [Alembic](http://alembic.zzzcomputing.com/en/latest/)
29 "migration environment" in `kallithea/alembic`, portions of which is:
30
31 Copyright &copy; 2009-2016 by Michael Bayer.
32 Alembic is a trademark of Michael Bayer.
33
34 and licensed under the MIT-permissive license, which is
35 [included in this distribution](MIT-Permissive-License.txt).
36
25
37
26 Bootstrap
38 Bootstrap
27 ---------
39 ---------
28
40
29 Kallithea incorporates parts of the Javascript system called
41 Kallithea uses the web framework called
30 [Bootstrap](http://getbootstrap.com/), which is:
42 [Bootstrap](http://getbootstrap.com/), which is:
31
43
32 Copyright &copy; 2012 Twitter, Inc.
44 Copyright &copy; 2011-2016 Twitter, Inc.
33
45
34 and licensed under
46 and licensed under the MIT-permissive license, which is
35 [the Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0.html).
47 [included in this distribution](MIT-Permissive-License.txt).
36
48
37 A copy of the Apache License 2.0 is also included in this distribution in its
49 It is not distributed with Kallithea, but will be downloaded
38 entirety in the file Apache-License-2.0.txt
50 using the ''kallithea-cli front-end-build'' command.
39
51
40
52
41
53
42 Codemirror
54 Codemirror
43 ----------
55 ----------
44
56
45 Kallithea incorporates parts of the Javascript system called
57 Kallithea uses the Javascript system called
46 [Codemirror](http://codemirror.net/), version 4.7.0, which is primarily:
58 [Codemirror](http://codemirror.net/), version 4.7.0, which is primarily:
47
59
48 Copyright &copy; 2013-2014 by Marijn Haverbeke <marijnh@gmail.com>
60 Copyright &copy; 2013-2014 by Marijn Haverbeke <marijnh@gmail.com>
@@ -51,40 +63,70 b' and licensed under the MIT-permissive li'
51 [included in this distribution](MIT-Permissive-License.txt).
63 [included in this distribution](MIT-Permissive-License.txt).
52
64
53 Additional files from upstream Codemirror are copyrighted by various authors
65 Additional files from upstream Codemirror are copyrighted by various authors
54 and licensed under other permissive licenses. The sub-directories under
66 and licensed under other permissive licenses.
55 [.../public/codemirror](kallithea/public/codemirror) include the copyright and
67
56 license notice and information as they appeared in Codemirror's upstream
68 It is not distributed with Kallithea, but will be downloaded
57 release.
69 using the ''kallithea-cli front-end-build'' command.
58
70
59
71
60
72
61 jQuery
73 jQuery
62 ------
74 ------
63
75
64 Kallithea incorporates the Javascript system called
76 Kallithea uses the Javascript system called
65 [jQuery](http://jquery.org/),
77 [jQuery](http://jquery.org/).
66 [herein](kallithea/public/js/jquery-1.11.1.min.js), and the Corresponding
67 Source can be found in https://github.com/jquery/jquery at tag 1.11.1
68 (mirrored at https://kallithea-scm.org/repos/mirror/jquery/files/1.11.1/ ).
69
78
70 It is Copyright 2013 jQuery Foundation and other contributors http://jquery.com/ and is under an
79 It is Copyright 2013 jQuery Foundation and other contributors http://jquery.com/ and is under an
71 [MIT-permissive license](MIT-Permissive-License.txt).
80 [MIT-permissive license](MIT-Permissive-License.txt).
72
81
82 It is not distributed with Kallithea, but will be downloaded
83 using the ''kallithea-cli front-end-build'' command.
84
85
86
87 At.js
88 -----
89
90 Kallithea uses the Javascript system called
91 [At.js](http://ichord.github.com/At.js),
92 which can be found together with its Corresponding Source in
93 https://github.com/ichord/At.js at tag v1.5.4.
94
95 It is Copyright 2013 chord.luo@gmail.com and is under an
96 [MIT-permissive license](MIT-Permissive-License.txt).
97
98 It is not distributed with Kallithea, but will be downloaded
99 using the ''kallithea-cli front-end-build'' command.
100
73
101
74
102
75 Mousetrap
103 Caret.js
76 ---------
104 --------
77
105
78 Kallithea incorporates parts of the Javascript system called
106 Kallithea uses the Javascript system called
79 [Mousetrap](http://craig.is/killing/mice/), which is:
107 [Caret.js](http://ichord.github.com/Caret.js/),
108 which can be found together with its Corresponding Source in
109 https://github.com/ichord/Caret.js at tag v0.3.1.
110
111 It is Copyright 2013 chord.luo@gmail.com and is under an
112 [MIT-permissive license](MIT-Permissive-License.txt).
113
114 It is not distributed with Kallithea, but will be downloaded
115 using the ''kallithea-cli front-end-build'' command.
80
116
81 Copyright 2013 Craig Campbell
117
118
119 DataTables
120 ----------
82
121
83 and licensed under
122 Kallithea uses the Javascript system called
84 [the Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0.html).
123 [DataTables](http://www.datatables.net/).
85
124
86 A [copy of the Apache License 2.0](Apache-License-2.0.txt) is also included
125 It is Copyright 2008-2015 SpryMedia Ltd. and is under an
87 in this distribution.
126 [MIT-permissive license](MIT-Permissive-License.txt).
127
128 It is not distributed with Kallithea, but will be downloaded
129 using the ''kallithea-cli front-end-build'' command.
88
130
89
131
90
132
@@ -103,7 +145,7 b' tri-license.'
103 Select2
145 Select2
104 -------
146 -------
105
147
106 Kallithea incorporates parts of the Javascript system called
148 Kallithea uses the Javascript system called
107 [Select2](http://ivaynberg.github.io/select2/), which is:
149 [Select2](http://ivaynberg.github.io/select2/), which is:
108
150
109 Copyright 2012 Igor Vaynberg (and probably others)
151 Copyright 2012 Igor Vaynberg (and probably others)
@@ -122,12 +164,15 b' in this distribution.'
122 Kallithea will take the Apache license fork of the dual license, since
164 Kallithea will take the Apache license fork of the dual license, since
123 Kallithea is GPLv3'd.
165 Kallithea is GPLv3'd.
124
166
167 It is not distributed with Kallithea, but will be downloaded
168 using the ''kallithea-cli front-end-build'' command.
169
125
170
126
171
127 Select2-Bootstrap-CSS
172 Select2-Bootstrap-CSS
128 ---------------------
173 ---------------------
129
174
130 Kallithea incorporates some CSS from a system called
175 Kallithea uses some CSS from a system called
131 [Select2-bootstrap-css](https://github.com/t0m/select2-bootstrap-css), which
176 [Select2-bootstrap-css](https://github.com/t0m/select2-bootstrap-css), which
132 is:
177 is:
133
178
@@ -136,122 +181,43 b' Copyright &copy; 2013 Tom Terrace (and l'
136 and licensed under the MIT-permissive license, which is
181 and licensed under the MIT-permissive license, which is
137 [included in this distribution](MIT-Permissive-License.txt).
182 [included in this distribution](MIT-Permissive-License.txt).
138
183
139
184 It is not distributed with Kallithea, but will be downloaded
140
185 using the ''kallithea-cli front-end-build'' command.
141 History.js
142 ----------
143
144 Kallithea incorporates some CSS from a system called History.js, which is
145
146 Copyright 2010-2011 Benjamin Arthur Lupton <contact@balupton.com>
147
148 Redistribution and use in source and binary forms, with or without
149 modification, are permitted provided that the following conditions are met:
150
151 1. Redistributions of source code must retain the above copyright notice,
152 this list of conditions and the following disclaimer.
153
154 2. Redistributions in binary form must reproduce the above copyright notice,
155 this list of conditions and the following disclaimer in the documentation
156 and/or other materials provided with the distribution.
157
158 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
159 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
160 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
161 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
162 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
163 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
164 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
165 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
166 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
167 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
168 POSSIBILITY OF SUCH DAMAGE.
169
170
171
172 YUI
173 ---
174
175 Kallithea incorporates parts of the Javascript system called
176 [YUI 2 — Yahoo! User Interface Library](http://yui.github.io/yui2/docs/yui_2.9.0_full/),
177 which is made available under the [BSD License](http://yuilibrary.com/license/):
178
179 Copyright &copy; 2013 Yahoo! Inc. All rights reserved.
180
181 Redistribution and use of this software in source and binary forms, with or
182 without modification, are permitted provided that the following conditions are
183 met:
184
185 * Redistributions of source code must retain the above copyright notice, this
186 list of conditions and the following disclaimer.
187
188 * Redistributions in binary form must reproduce the above copyright notice,
189 this list of conditions and the following disclaimer in the documentation
190 and/or other materials provided with the distribution.
191
192 * Neither the name of Yahoo! Inc. nor the names of YUI's contributors may be
193 used to endorse or promote products derived from this software without
194 specific prior written permission of Yahoo! Inc.
195
196 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
197 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
198 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
199 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
200 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
201 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
202 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
203 ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
204 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
205 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
206
207
208 Kallithea includes a minified version of YUI 2.9. To build yui.2.9.js:
209
210 git clone https://github.com/yui/builder
211 git clone https://github.com/yui/yui2
212 cd yui2/
213 git checkout hudson-yui2-2800
214 ln -sf JumpToPageDropDown.js src/paginator/js/JumpToPageDropdown.js # work around inconsistent casing
215 rm -f tmp.js
216 for m in yahoo event dom connection animation dragdrop element datasource autocomplete container event-delegate json datatable paginator; do
217 rm -f build/\$m/\$m.js
218 ( cd src/\$m && ant build deploybuild ) && sed -e 's,@VERSION@,2.9.0,g' -e 's,@BUILD@,2800,g' build/\$m/\$m.js >> tmp.js
219 done
220 java -jar ../builder/componentbuild/lib/yuicompressor/yuicompressor-2.4.4.jar tmp.js -o yui.2.9.js
221
222 In compliance with GPLv3 the Corresponding Source for this Object Code is made
223 available on
224 [https://kallithea-scm.org/repos/mirror](https://kallithea-scm.org/repos/mirror).
225
186
226
187
227
188
228 Flot
189 Flot
229 ----
190 ----
230
191
231 Kallithea incorporates some CSS from a system called
192 Kallithea uses some parts of a Javascript system called
232 [Flot](http://code.google.com/p/flot/), which is:
193 [Flot](http://www.flotcharts.org/), which is:
233
194
234 Copyright 2006 Google Inc.
195 Copyright (c) 2007-2014 IOLA and Ole Laursen
235
196
236 Licensed under the Apache License, Version 2.0 (the "License");
197 Permission is hereby granted, free of charge, to any person
237 you may not use this file except in compliance with the License.
198 obtaining a copy of this software and associated documentation
238
199 files (the "Software"), to deal in the Software without
239 A [copy of the Apache License 2.0](Apache-License-2.0.txt) is also included
200 restriction, including without limitation the rights to use,
240 in this distribution.
201 copy, modify, merge, publish, distribute, sublicense, and/or sell
241
202 copies of the Software, and to permit persons to whom the
203 Software is furnished to do so, subject to the following
204 conditions:
242
205
243
206 The above copyright notice and this permission notice shall be
244 Migrate
207 included in all copies or substantial portions of the Software.
245 -------
246
208
247 Kallithea incorporates in kallithea/lib/dbmigrate/migrate parts of the Python
209 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
248 system called [Migrate or sqlalchemy-migrate](https://github.com/stackforge/sqlalchemy-migrate),
210 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
249 which is:
211 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
212 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
213 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
214 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
215 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
216 OTHER DEALINGS IN THE SOFTWARE.
250
217
251 Copyright (c) 2009 Evan Rosson, Jan Dittberner, Domen Kožar
218 It is not distributed with Kallithea, but will be downloaded
219 using the ''kallithea-cli front-end-build'' command.
252
220
253 and licensed under the MIT-permissive license, which is
254 [included in this distribution](MIT-Permissive-License.txt).
255
221
256
222
257 Icon fonts
223 Icon fonts
@@ -261,7 +227,7 b' Kallithea incorporates subsets of both'
261 [Font Awesome](http://fontawesome.io) and
227 [Font Awesome](http://fontawesome.io) and
262 [GitHub Octicons](https://octicons.github.com) for icons. Font Awesome is:
228 [GitHub Octicons](https://octicons.github.com) for icons. Font Awesome is:
263
229
264 Copyright (c) 2012, Dave Gandy
230 Copyright (c) 2016, Dave Gandy
265
231
266 Octicons is:
232 Octicons is:
267
233
@@ -1,21 +1,29 b''
1 include .coveragerc
1 include Apache-License-2.0.txt
2 include Apache-License-2.0.txt
2 include CONTRIBUTORS
3 include CONTRIBUTORS
3 include COPYING
4 include COPYING
5 include Jenkinsfile
4 include LICENSE-MERGELY.html
6 include LICENSE-MERGELY.html
5 include LICENSE.md
7 include LICENSE.md
6 include MIT-Permissive-License.txt
8 include MIT-Permissive-License.txt
7 include README.rst
9 include README.rst
10 include dev_requirements.txt
8 include development.ini
11 include development.ini
12 include pytest.ini
13 include requirements.txt
14 include tox.ini
9 recursive-include docs *
15 recursive-include docs *
10 recursive-include init.d *
16 recursive-include init.d *
17 recursive-include kallithea/alembic *
11 include kallithea/bin/ldap_sync.conf
18 include kallithea/bin/ldap_sync.conf
12 include kallithea/bin/template.ini.mako
19 include kallithea/lib/paster_commands/template.ini.mako
13 include kallithea/config/deployment.ini_tmpl
20 recursive-include kallithea/front-end *
14 recursive-include kallithea/i18n *
21 recursive-include kallithea/i18n *
15 recursive-include kallithea/lib/dbmigrate *.py_tmpl README migrate.cfg
16 recursive-include kallithea/public *
22 recursive-include kallithea/public *
17 recursive-include kallithea/templates *
23 recursive-include kallithea/templates *
18 recursive-include kallithea/tests/fixtures *
24 recursive-include kallithea/tests/fixtures *
19 recursive-include kallithea/tests/scripts *
25 recursive-include kallithea/tests/scripts *
20 include kallithea/tests/test.ini
26 include kallithea/tests/models/test_dump_html_mails.ref.html
27 include kallithea/tests/performance/test_vcs.py
21 include kallithea/tests/vcs/aconfig
28 include kallithea/tests/vcs/aconfig
29 recursive-include scripts *
@@ -162,76 +162,14 b' You can also build the documentation loc'
162 install it via the command: ``pip install sphinx`` .
162 install it via the command: ``pip install sphinx`` .
163
163
164
164
165 Converting from RhodeCode
165 Migrating from RhodeCode
166 -------------------------
166 ------------------------
167
168 Currently, you have two options for working with an existing RhodeCode
169 database:
170
171 - keep the database unconverted (intended for testing and evaluation)
172 - convert the database in a one-time step
173
174 Maintaining interoperability
175 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
176
177 Interoperability with RhodeCode 2.2.X installations is provided so you don't
178 have to immediately commit to switching to Kallithea. This option will most
179 likely go away once the two projects have diverged significantly.
180
181 To run Kallithea on a RhodeCode database, run::
182
183 echo "BRAND = 'rhodecode'" > kallithea/brand.py
184
185 This location will depend on where you installed Kallithea. If you installed
186 via::
187
188 python2 setup.py install
189
190 then you will find this location at
191 ``$VIRTUAL_ENV/lib/python2.7/site-packages/Kallithea-0.1-py2.7.egg/kallithea``.
192
193 One-time conversion
194 ~~~~~~~~~~~~~~~~~~~
195
167
196 Alternatively, if you would like to convert the database for good, you can use
168 Kallithea 0.3.2 and earlier supports migrating from an existing RhodeCode
197 a helper script provided by Kallithea. This script will operate directly on the
169 installation. To migrate, install Kallithea 0.3.2 and follow the
198 database, using the database string you can find in your ``production.ini`` (or
170 instructions in the 0.3.2 README to perform a one-time conversion of the
199 ``development.ini``) file. For example, if using SQLite::
171 database from RhodeCode to Kallithea, before upgrading to this version
200
172 of Kallithea.
201 cd /path/to/kallithea
202 cp /path/to/rhodecode/rhodecode.db kallithea.db
203 pip install sqlalchemy-migrate
204 python2 kallithea/bin/rebranddb.py sqlite:///kallithea.db
205
206 .. Note::
207
208 If you started out using the branding interoperability approach mentioned
209 above, watch out for stray brand.pyc after removing brand.py.
210
211 Git hooks
212 ~~~~~~~~~
213
214 After switching to Kallithea, it will be necessary to update the Git_ hooks in
215 your repositories. If not, the Git_ hooks from RhodeCode will still be called,
216 which will cause ``git push`` to fail every time.
217
218 If you do not have any custom Git_ hooks deployed, perform the following steps
219 (this may take some time depending on the number and size of repositories you
220 have):
221
222 1. Log-in as an administrator.
223
224 2. Open page *Admin > Settings > Remap and Rescan*.
225
226 3. Turn on the option **Install Git Hooks**.
227
228 4. Turn on the option **Overwrite existing Git hooks**.
229
230 5. Click on the button **Rescan Repositories**.
231
232 If you do have custom hooks, you will need to merge those changes manually. In
233 order to get sample hooks from Kallithea, the easiest way is to create a new Git_
234 repository, and have a look at the hooks deployed there.
235
173
236
174
237 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
175 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
@@ -1,19 +1,12 b''
1 ################################################################################
1 ################################################################################
2 ################################################################################
2 ################################################################################
3 # Kallithea - Development config: #
3 # Kallithea - config file generated with kallithea-config #
4 # listening on *:5000 #
5 # sqlite and kallithea.db #
6 # initial_repo_scan = true #
7 # set debug = true #
8 # verbose and colorful logging #
9 # #
4 # #
10 # The %(here)s variable will be replaced with the parent directory of this file#
5 # The %(here)s variable will be replaced with the parent directory of this file#
11 ################################################################################
6 ################################################################################
12 ################################################################################
7 ################################################################################
13
8
14 [DEFAULT]
9 [DEFAULT]
15 debug = true
16 pdebug = false
17
10
18 ################################################################################
11 ################################################################################
19 ## Email settings ##
12 ## Email settings ##
@@ -39,43 +32,39 b' pdebug = false'
39 #email_prefix = [Kallithea]
32 #email_prefix = [Kallithea]
40
33
41 ## Recipients for error emails and fallback recipients of application mails.
34 ## Recipients for error emails and fallback recipients of application mails.
42 ## Multiple addresses can be specified, space-separated.
35 ## Multiple addresses can be specified, comma-separated.
43 ## Only addresses are allowed, do not add any name part.
36 ## Only addresses are allowed, do not add any name part.
44 ## Default:
37 ## Default:
45 #email_to =
38 #email_to =
46 ## Examples:
39 ## Examples:
47 #email_to = admin@example.com
40 #email_to = admin@example.com
48 #email_to = admin@example.com another_admin@example.com
41 #email_to = admin@example.com,another_admin@example.com
42 email_to =
49
43
50 ## 'From' header for error emails. You can optionally add a name.
44 ## 'From' header for error emails. You can optionally add a name.
51 ## Default:
45 ## Default: (none)
52 #error_email_from = pylons@yourapp.com
53 ## Examples:
46 ## Examples:
54 #error_email_from = Kallithea Errors <kallithea-noreply@example.com>
47 #error_email_from = Kallithea Errors <kallithea-noreply@example.com>
55 #error_email_from = paste_error@example.com
48 #error_email_from = kallithea_errors@example.com
49 error_email_from =
56
50
57 ## SMTP server settings
51 ## SMTP server settings
58 ## Only smtp_server is mandatory. All other settings take the specified default
52 ## If specifying credentials, make sure to use secure connections.
59 ## values.
53 ## Default: Send unencrypted unauthenticated mails to the specified smtp_server.
60 #smtp_server = smtp.example.com
54 ## For "SSL", use smtp_use_ssl = true and smtp_port = 465.
55 ## For "STARTTLS", use smtp_use_tls = true and smtp_port = 587.
56 smtp_server =
61 #smtp_username =
57 #smtp_username =
62 #smtp_password =
58 #smtp_password =
63 #smtp_port = 25
59 smtp_port =
64 #smtp_use_tls = false
65 #smtp_use_ssl = false
60 #smtp_use_ssl = false
66 ## SMTP authentication parameters to use (e.g. LOGIN PLAIN CRAM-MD5, etc.).
61 #smtp_use_tls = false
67 ## If empty, use any of the authentication parameters supported by the server.
68 #smtp_auth =
69
62
63 ## Entry point for 'gearbox serve'
70 [server:main]
64 [server:main]
71 ## PASTE ##
65 #host = 127.0.0.1
72 #use = egg:Paste#http
66 host = 0.0.0.0
73 ## nr of worker threads to spawn
67 port = 5000
74 #threadpool_workers = 1
75 ## max request before thread respawn
76 #threadpool_max_requests = 100
77 ## option to use threads of process
78 #use_threadpool = true
79
68
80 ## WAITRESS ##
69 ## WAITRESS ##
81 use = egg:waitress#main
70 use = egg:waitress#main
@@ -87,85 +76,6 b' max_request_body_size = 107374182400'
87 ## windows systems.
76 ## windows systems.
88 #asyncore_use_poll = True
77 #asyncore_use_poll = True
89
78
90 ## GUNICORN ##
91 #use = egg:gunicorn#main
92 ## number of process workers. You must set `instance_id = *` when this option
93 ## is set to more than one worker
94 #workers = 1
95 ## process name
96 #proc_name = kallithea
97 ## type of worker class, one of sync, eventlet, gevent, tornado
98 ## recommended for bigger setup is using of of other than sync one
99 #worker_class = sync
100 #max_requests = 1000
101 ## ammount of time a worker can handle request before it gets killed and
102 ## restarted
103 #timeout = 3600
104
105 ## UWSGI ##
106 ## run with uwsgi --ini-paste-logged <inifile.ini>
107 #[uwsgi]
108 #socket = /tmp/uwsgi.sock
109 #master = true
110 #http = 127.0.0.1:5000
111
112 ## set as deamon and redirect all output to file
113 #daemonize = ./uwsgi_kallithea.log
114
115 ## master process PID
116 #pidfile = ./uwsgi_kallithea.pid
117
118 ## stats server with workers statistics, use uwsgitop
119 ## for monitoring, `uwsgitop 127.0.0.1:1717`
120 #stats = 127.0.0.1:1717
121 #memory-report = true
122
123 ## log 5XX errors
124 #log-5xx = true
125
126 ## Set the socket listen queue size.
127 #listen = 256
128
129 ## Gracefully Reload workers after the specified amount of managed requests
130 ## (avoid memory leaks).
131 #max-requests = 1000
132
133 ## enable large buffers
134 #buffer-size = 65535
135
136 ## socket and http timeouts ##
137 #http-timeout = 3600
138 #socket-timeout = 3600
139
140 ## Log requests slower than the specified number of milliseconds.
141 #log-slow = 10
142
143 ## Exit if no app can be loaded.
144 #need-app = true
145
146 ## Set lazy mode (load apps in workers instead of master).
147 #lazy = true
148
149 ## scaling ##
150 ## set cheaper algorithm to use, if not set default will be used
151 #cheaper-algo = spare
152
153 ## minimum number of workers to keep at all times
154 #cheaper = 1
155
156 ## number of workers to spawn at startup
157 #cheaper-initial = 1
158
159 ## maximum number of workers that can be spawned
160 #workers = 4
161
162 ## how many workers should be spawned at a time
163 #cheaper-step = 1
164
165 ## COMMON ##
166 host = 0.0.0.0
167 port = 5000
168
169 ## middleware for hosting the WSGI application under a URL prefix
79 ## middleware for hosting the WSGI application under a URL prefix
170 #[filter:proxy-prefix]
80 #[filter:proxy-prefix]
171 #use = egg:PasteDeploy#prefix
81 #use = egg:PasteDeploy#prefix
@@ -178,29 +88,26 b' use = egg:kallithea'
178
88
179 full_stack = true
89 full_stack = true
180 static_files = true
90 static_files = true
181 ## Available Languages:
91
182 ## cs de fr hu ja nl_BE pl pt_BR ru sk zh_CN zh_TW
92 ## Internationalization (see setup documentation for details)
183 lang =
93 ## By default, the language requested by the browser is used if available.
94 #i18n.enable = false
95 ## Fallback language, empty for English (valid values are the names of subdirectories in kallithea/i18n):
96 i18n.lang =
97
184 cache_dir = %(here)s/data
98 cache_dir = %(here)s/data
185 index_dir = %(here)s/data/index
99 index_dir = %(here)s/data/index
186
100
187 ## perform a full repository scan on each server start, this should be
188 ## set to false after first startup, to allow faster server restarts.
189 #initial_repo_scan = false
190 initial_repo_scan = true
191
192 ## uncomment and set this path to use archive download cache
101 ## uncomment and set this path to use archive download cache
193 archive_cache_dir = %(here)s/tarballcache
102 archive_cache_dir = %(here)s/tarballcache
194
103
195 ## change this to unique ID for security
104 ## change this to unique ID for security
105 #app_instance_uuid = VERY-SECRET
196 app_instance_uuid = development-not-secret
106 app_instance_uuid = development-not-secret
197
107
198 ## cut off limit for large diffs (size in bytes)
108 ## cut off limit for large diffs (size in bytes)
199 cut_off_limit = 256000
109 cut_off_limit = 256000
200
110
201 ## use cache version of scm repo everywhere
202 vcs_full_cache = true
203
204 ## force https in Kallithea, fixes https redirects, assumes it's always https
111 ## force https in Kallithea, fixes https redirects, assumes it's always https
205 force_https = false
112 force_https = false
206
113
@@ -226,6 +133,11 b' rss_include_diff = false'
226 show_sha_length = 12
133 show_sha_length = 12
227 show_revision_number = false
134 show_revision_number = false
228
135
136 ## Canonical URL to use when creating full URLs in UI and texts.
137 ## Useful when the site is available under different names or protocols.
138 ## Defaults to what is provided in the WSGI environment.
139 #canonical_url = https://kallithea.example.com/repos
140
229 ## gist URL alias, used to create nicer urls for gist. This should be an
141 ## gist URL alias, used to create nicer urls for gist. This should be an
230 ## url that does rewrites to _admin/gists/<gistid>.
142 ## url that does rewrites to _admin/gists/<gistid>.
231 ## example: http://gist.example.com/{gistid}. Empty means use the internal
143 ## example: http://gist.example.com/{gistid}. Empty means use the internal
@@ -245,46 +157,53 b' api_access_controllers_whitelist ='
245 # FilesController:archivefile
157 # FilesController:archivefile
246
158
247 ## default encoding used to convert from and to unicode
159 ## default encoding used to convert from and to unicode
248 ## can be also a comma seperated list of encoding in case of mixed encodings
160 ## can be also a comma separated list of encoding in case of mixed encodings
249 default_encoding = utf8
161 default_encoding = utf-8
162
163 ## Set Mercurial encoding, similar to setting HGENCODING before launching Kallithea
164 hgencoding = utf-8
250
165
251 ## issue tracker for Kallithea (leave blank to disable, absent for default)
166 ## issue tracker for Kallithea (leave blank to disable, absent for default)
252 #bugtracker = https://bitbucket.org/conservancy/kallithea/issues
167 #bugtracker = https://bitbucket.org/conservancy/kallithea/issues
253
168
254 ## issue tracking mapping for commits messages
169 ## issue tracking mapping for commit messages, comments, PR descriptions, ...
255 ## comment out issue_pat, issue_server, issue_prefix to enable
170 ## Refer to the documentation ("Integration with issue trackers") for more details.
256
171
257 ## pattern to get the issues from commit messages
172 ## regular expression to match issue references
258 ## default one used here is #<numbers> with a regex passive group for `#`
173 ## This pattern may/should contain parenthesized groups, that can
259 ## {id} will be all groups matched from this pattern
174 ## be referred to in issue_server_link or issue_sub using Python backreferences
175 ## (e.g. \1, \2, ...). You can also create named groups with '(?P<groupname>)'.
176 ## To require mandatory whitespace before the issue pattern, use:
177 ## (?:^|(?<=\s)) before the actual pattern, and for mandatory whitespace
178 ## behind the issue pattern, use (?:$|(?=\s)) after the actual pattern.
260
179
261 issue_pat = (?:\s*#)(\d+)
180 issue_pat = #(\d+)
262
181
263 ## server url to the issue, each {id} will be replaced with match
182 ## server url to the issue
264 ## fetched from the regex and {repo} is replaced with full repository name
183 ## This pattern may/should contain backreferences to parenthesized groups in issue_pat.
265 ## including groups {repo_name} is replaced with just name of repo
184 ## A backreference can be \1, \2, ... or \g<groupname> if you specified a named group
266
185 ## called 'groupname' in issue_pat.
267 issue_server_link = https://issues.example.com/{repo}/issue/{id}
186 ## The special token {repo} is replaced with the full repository name
187 ## including repository groups, while {repo_name} is replaced with just
188 ## the name of the repository.
268
189
269 ## prefix to add to link to indicate it's an url
190 issue_server_link = https://issues.example.com/{repo}/issue/\1
270 ## #314 will be replaced by <issue_prefix><id>
271
191
272 issue_prefix = #
192 ## substitution pattern to use as the link text
193 ## If issue_sub is empty, the text matched by issue_pat is retained verbatim
194 ## for the link text. Otherwise, the link text is that of issue_sub, with any
195 ## backreferences to groups in issue_pat replaced.
273
196
274 ## issue_pat, issue_server_link, issue_prefix can have suffixes to specify
197 issue_sub =
198
199 ## issue_pat, issue_server_link and issue_sub can have suffixes to specify
275 ## multiple patterns, to other issues server, wiki or others
200 ## multiple patterns, to other issues server, wiki or others
276 ## below an example how to create a wiki pattern
201 ## below an example how to create a wiki pattern
277 # wiki-some-id -> https://wiki.example.com/some-id
202 # wiki-some-id -> https://wiki.example.com/some-id
278
203
279 #issue_pat_wiki = (?:wiki-)(.+)
204 #issue_pat_wiki = wiki-(\S+)
280 #issue_server_link_wiki = https://wiki.example.com/{id}
205 #issue_server_link_wiki = https://wiki.example.com/\1
281 #issue_prefix_wiki = WIKI-
206 #issue_sub_wiki = WIKI-\1
282
283 ## instance-id prefix
284 ## a prefix key for this instance used for cache invalidation when running
285 ## multiple instances of kallithea, make sure it's globally unique for
286 ## all running kallithea instances. Leave empty if you don't use it
287 instance_id =
288
207
289 ## alternative return HTTP header for failed authentication. Default HTTP
208 ## alternative return HTTP header for failed authentication. Default HTTP
290 ## response is 401 HTTPUnauthorized. Currently Mercurial clients have trouble with
209 ## response is 401 HTTPUnauthorized. Currently Mercurial clients have trouble with
@@ -301,19 +220,29 b' allow_repo_location_change = True'
301 ## allows to setup custom hooks in settings page
220 ## allows to setup custom hooks in settings page
302 allow_custom_hooks_settings = True
221 allow_custom_hooks_settings = True
303
222
223 ## extra extensions for indexing, space separated and without the leading '.'.
224 # index.extensions =
225 # gemfile
226 # lock
227
228 ## extra filenames for indexing, space separated
229 # index.filenames =
230 # .dockerignore
231 # .editorconfig
232 # INSTALL
233 # CHANGELOG
234
304 ####################################
235 ####################################
305 ### CELERY CONFIG ####
236 ### CELERY CONFIG ####
306 ####################################
237 ####################################
307
238
308 use_celery = false
239 use_celery = false
309 broker.host = localhost
240
310 broker.vhost = rabbitmqhost
241 ## Example: connect to the virtual host 'rabbitmqhost' on localhost as rabbitmq:
311 broker.port = 5672
242 broker.url = amqp://rabbitmq:qewqew@localhost:5672/rabbitmqhost
312 broker.user = rabbitmq
313 broker.password = qweqwe
314
243
315 celery.imports = kallithea.lib.celerylib.tasks
244 celery.imports = kallithea.lib.celerylib.tasks
316
245 celery.accept.content = pickle
317 celery.result.backend = amqp
246 celery.result.backend = amqp
318 celery.result.dburi = amqp://
247 celery.result.dburi = amqp://
319 celery.result.serialier = json
248 celery.result.serialier = json
@@ -322,11 +251,9 b' celery.result.serialier = json'
322 #celery.amqp.task.result.expires = 18000
251 #celery.amqp.task.result.expires = 18000
323
252
324 celeryd.concurrency = 2
253 celeryd.concurrency = 2
325 #celeryd.log.file = celeryd.log
326 celeryd.log.level = DEBUG
327 celeryd.max.tasks.per.child = 1
254 celeryd.max.tasks.per.child = 1
328
255
329 ## tasks will never be sent to the queue, but executed locally instead.
256 ## If true, tasks will never be sent to the queue, but executed locally instead.
330 celery.always.eager = false
257 celery.always.eager = false
331
258
332 ####################################
259 ####################################
@@ -363,6 +290,7 b' beaker.session.httponly = true'
363 beaker.session.timeout = 2592000
290 beaker.session.timeout = 2592000
364
291
365 ## Server secret used with HMAC to ensure integrity of cookies.
292 ## Server secret used with HMAC to ensure integrity of cookies.
293 #beaker.session.secret = VERY-SECRET
366 beaker.session.secret = development-not-secret
294 beaker.session.secret = development-not-secret
367 ## Further, encrypt the data with AES.
295 ## Further, encrypt the data with AES.
368 #beaker.session.encrypt_key = <key_for_encryption>
296 #beaker.session.encrypt_key = <key_for_encryption>
@@ -382,90 +310,13 b' beaker.session.secret = development-not-'
382 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/kallithea
310 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/kallithea
383 #beaker.session.table_name = db_session
311 #beaker.session.table_name = db_session
384
312
385 ############################
386 ## ERROR HANDLING SYSTEMS ##
387 ############################
388
389 ####################
390 ### [appenlight] ###
391 ####################
392
393 ## AppEnlight is tailored to work with Kallithea, see
394 ## http://appenlight.com for details how to obtain an account
395 ## you must install python package `appenlight_client` to make it work
396
397 ## appenlight enabled
398 appenlight = false
399
400 appenlight.server_url = https://api.appenlight.com
401 appenlight.api_key = YOUR_API_KEY
402
403 ## TWEAK AMOUNT OF INFO SENT HERE
404
405 ## enables 404 error logging (default False)
406 appenlight.report_404 = false
407
408 ## time in seconds after request is considered being slow (default 1)
409 appenlight.slow_request_time = 1
410
411 ## record slow requests in application
412 ## (needs to be enabled for slow datastore recording and time tracking)
413 appenlight.slow_requests = true
414
415 ## enable hooking to application loggers
416 #appenlight.logging = true
417
418 ## minimum log level for log capture
419 #appenlight.logging.level = WARNING
420
421 ## send logs only from erroneous/slow requests
422 ## (saves API quota for intensive logging)
423 appenlight.logging_on_error = false
424
425 ## list of additonal keywords that should be grabbed from environ object
426 ## can be string with comma separated list of words in lowercase
427 ## (by default client will always send following info:
428 ## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
429 ## start with HTTP* this list be extended with additional keywords here
430 appenlight.environ_keys_whitelist =
431
432 ## list of keywords that should be blanked from request object
433 ## can be string with comma separated list of words in lowercase
434 ## (by default client will always blank keys that contain following words
435 ## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
436 ## this list be extended with additional keywords set here
437 appenlight.request_keys_blacklist =
438
439 ## list of namespaces that should be ignores when gathering log entries
440 ## can be string with comma separated list of namespaces
441 ## (by default the client ignores own entries: appenlight_client.client)
442 appenlight.log_namespace_blacklist =
443
444 ################
445 ### [sentry] ###
446 ################
447
448 ## sentry is a alternative open source error aggregator
449 ## you must install python packages `sentry` and `raven` to enable
450
451 sentry.dsn = YOUR_DNS
452 sentry.servers =
453 sentry.name =
454 sentry.key =
455 sentry.public_key =
456 sentry.secret_key =
457 sentry.project =
458 sentry.site =
459 sentry.include_paths =
460 sentry.exclude_paths =
461
462 ################################################################################
313 ################################################################################
463 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
314 ## WARNING: *DEBUG MODE MUST BE OFF IN A PRODUCTION ENVIRONMENT* ##
464 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
315 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
465 ## execute malicious code after an exception is raised. ##
316 ## execute malicious code after an exception is raised. ##
466 ################################################################################
317 ################################################################################
467 #set debug = false
318 #debug = false
468 set debug = true
319 debug = true
469
320
470 ##################################
321 ##################################
471 ### LOGVIEW CONFIG ###
322 ### LOGVIEW CONFIG ###
@@ -480,26 +331,25 b' logview.pylons.util = #eee'
480 #########################################################
331 #########################################################
481
332
482 # SQLITE [default]
333 # SQLITE [default]
483 sqlalchemy.db1.url = sqlite:///%(here)s/kallithea.db?timeout=60
334 sqlalchemy.url = sqlite:///%(here)s/kallithea.db?timeout=60
484
485 # POSTGRESQL
486 #sqlalchemy.db1.url = postgresql://user:pass@localhost/kallithea
487
488 # MySQL
489 #sqlalchemy.db1.url = mysql://user:pass@localhost/kallithea
490
335
491 # see sqlalchemy docs for others
336 # see sqlalchemy docs for others
492
337
493 sqlalchemy.db1.echo = false
338 sqlalchemy.pool_recycle = 3600
494 sqlalchemy.db1.pool_recycle = 3600
339
495 sqlalchemy.db1.convert_unicode = true
340 ################################
341 ### ALEMBIC CONFIGURATION ####
342 ################################
343
344 [alembic]
345 script_location = kallithea:alembic
496
346
497 ################################
347 ################################
498 ### LOGGING CONFIGURATION ####
348 ### LOGGING CONFIGURATION ####
499 ################################
349 ################################
500
350
501 [loggers]
351 [loggers]
502 keys = root, routes, kallithea, sqlalchemy, beaker, templates, whoosh_indexer
352 keys = root, routes, kallithea, sqlalchemy, tg, gearbox, beaker, templates, whoosh_indexer, werkzeug, backlash
503
353
504 [handlers]
354 [handlers]
505 keys = console, console_sql
355 keys = console, console_sql
@@ -516,6 +366,7 b' level = NOTSET'
516 handlers = console
366 handlers = console
517
367
518 [logger_routes]
368 [logger_routes]
369 #level = WARN
519 level = DEBUG
370 level = DEBUG
520 handlers =
371 handlers =
521 qualname = routes.middleware
372 qualname = routes.middleware
@@ -523,35 +374,65 b' qualname = routes.middleware'
523 propagate = 1
374 propagate = 1
524
375
525 [logger_beaker]
376 [logger_beaker]
377 #level = WARN
526 level = DEBUG
378 level = DEBUG
527 handlers =
379 handlers =
528 qualname = beaker.container
380 qualname = beaker.container
529 propagate = 1
381 propagate = 1
530
382
531 [logger_templates]
383 [logger_templates]
384 #level = WARN
532 level = INFO
385 level = INFO
533 handlers =
386 handlers =
534 qualname = pylons.templating
387 qualname = pylons.templating
535 propagate = 1
388 propagate = 1
536
389
537 [logger_kallithea]
390 [logger_kallithea]
391 #level = WARN
538 level = DEBUG
392 level = DEBUG
539 handlers =
393 handlers =
540 qualname = kallithea
394 qualname = kallithea
541 propagate = 1
395 propagate = 1
542
396
397 [logger_tg]
398 #level = WARN
399 level = DEBUG
400 handlers =
401 qualname = tg
402 propagate = 1
403
404 [logger_gearbox]
405 #level = WARN
406 level = DEBUG
407 handlers =
408 qualname = gearbox
409 propagate = 1
410
543 [logger_sqlalchemy]
411 [logger_sqlalchemy]
544 level = INFO
412 level = WARN
545 handlers = console_sql
413 handlers = console_sql
546 qualname = sqlalchemy.engine
414 qualname = sqlalchemy.engine
547 propagate = 0
415 propagate = 0
548
416
549 [logger_whoosh_indexer]
417 [logger_whoosh_indexer]
418 #level = WARN
550 level = DEBUG
419 level = DEBUG
551 handlers =
420 handlers =
552 qualname = whoosh_indexer
421 qualname = whoosh_indexer
553 propagate = 1
422 propagate = 1
554
423
424 [logger_werkzeug]
425 level = WARN
426 handlers =
427 qualname = werkzeug
428 propagate = 1
429
430 [logger_backlash]
431 level = WARN
432 handlers =
433 qualname = backlash
434 propagate = 1
435
555 ##############
436 ##############
556 ## HANDLERS ##
437 ## HANDLERS ##
557 ##############
438 ##############
@@ -559,17 +440,13 b' propagate = 1'
559 [handler_console]
440 [handler_console]
560 class = StreamHandler
441 class = StreamHandler
561 args = (sys.stderr,)
442 args = (sys.stderr,)
562 #level = INFO
563 #formatter = generic
443 #formatter = generic
564 level = DEBUG
565 formatter = color_formatter
444 formatter = color_formatter
566
445
567 [handler_console_sql]
446 [handler_console_sql]
568 class = StreamHandler
447 class = StreamHandler
569 args = (sys.stderr,)
448 args = (sys.stderr,)
570 #level = WARN
571 #formatter = generic
449 #formatter = generic
572 level = DEBUG
573 formatter = color_formatter_sql
450 formatter = color_formatter_sql
574
451
575 ################
452 ################
This diff has been collapsed as it changes many lines, (668 lines changed) Show them Hide them
@@ -1,164 +1,18 b''
1 .. _setup:
1 .. _authentication:
2
3 =====
4 Setup
5 =====
6
7
8 Setting up Kallithea
9 --------------------
10
11 First, you will need to create a Kallithea configuration file. Run the
12 following command to do so::
13
14 paster make-config Kallithea my.ini
15
16 This will create the file ``my.ini`` in the current directory. This
17 configuration file contains the various settings for Kallithea, e.g.
18 proxy port, email settings, usage of static files, cache, Celery
19 settings, and logging.
20
21 Next, you need to create the databases used by Kallithea. It is recommended to
22 use PostgreSQL or SQLite (default). If you choose a database other than the
23 default, ensure you properly adjust the database URL in your ``my.ini``
24 configuration file to use this other database. Kallithea currently supports
25 PostgreSQL, SQLite and MySQL databases. Create the database by running
26 the following command::
27
28 paster setup-db my.ini
29
30 This will prompt you for a "root" path. This "root" path is the location where
31 Kallithea will store all of its repositories on the current machine. After
32 entering this "root" path ``setup-db`` will also prompt you for a username
33 and password for the initial admin account which ``setup-db`` sets
34 up for you.
35
36 The ``setup-db`` values can also be given on the command line.
37 Example::
38
39 paster setup-db my.ini --user=nn --password=secret --email=nn@example.com --repos=/srv/repos
40
41 The ``setup-db`` command will create all needed tables and an
42 admin account. When choosing a root path you can either use a new
43 empty location, or a location which already contains existing
44 repositories. If you choose a location which contains existing
45 repositories Kallithea will add all of the repositories at the chosen
46 location to its database. (Note: make sure you specify the correct
47 path to the root).
48
49 .. note:: the given path for Mercurial_ repositories **must** be write
50 accessible for the application. It's very important since
51 the Kallithea web interface will work without write access,
52 but when trying to do a push it will fail with permission
53 denied errors unless it has write access.
54
55 You are now ready to use Kallithea. To run it simply execute::
56
57 paster serve my.ini
58
59 - This command runs the Kallithea server. The web app should be available at
60 http://127.0.0.1:5000. The IP address and port is configurable via the
61 configuration file created in the previous step.
62 - Log in to Kallithea using the admin account created when running ``setup-db``.
63 - The default permissions on each repository is read, and the owner is admin.
64 Remember to update these if needed.
65 - In the admin panel you can toggle LDAP, anonymous, and permissions
66 settings, as well as edit more advanced options on users and
67 repositories.
68
69
70 Extensions
71 ----------
72
73 Optionally one can create an ``rcextensions`` package that extends Kallithea
74 functionality.
75 To generate a skeleton extensions package, run::
76
77 paster make-rcext my.ini
78
2
79 This will create an ``rcextensions`` package next to the specified ``ini`` file.
3 ====================
80 With ``rcextensions`` it's possible to add additional mapping for whoosh,
4 Authentication setup
81 stats and add additional code into the push/pull/create/delete repo hooks,
5 ====================
82 for example for sending signals to build-bots such as Jenkins.
83
84 See the ``__init__.py`` file inside the generated ``rcextensions`` package
85 for more details.
86
87
88 Using Kallithea with SSH
89 ------------------------
90
91 Kallithea currently only hosts repositories using http and https. (The addition
92 of ssh hosting is a planned future feature.) However you can easily use ssh in
93 parallel with Kallithea. (Repository access via ssh is a standard "out of
94 the box" feature of Mercurial_ and you can use this to access any of the
95 repositories that Kallithea is hosting. See PublishingRepositories_)
96
97 Kallithea repository structures are kept in directories with the same name
98 as the project. When using repository groups, each group is a subdirectory.
99 This allows you to easily use ssh for accessing repositories.
100
101 In order to use ssh you need to make sure that your web server and the users'
102 login accounts have the correct permissions set on the appropriate directories.
103
104 .. note:: These permissions are independent of any permissions you
105 have set up using the Kallithea web interface.
106
107 If your main directory (the same as set in Kallithea settings) is for
108 example set to ``/srv/repos`` and the repository you are using is
109 named ``kallithea``, then to clone via ssh you should run::
110
111 hg clone ssh://user@kallithea.example.com/srv/repos/kallithea
112
113 Using other external tools such as mercurial-server_ or using ssh key-based
114 authentication is fully supported.
115
6
116 .. note:: In an advanced setup, in order for your ssh access to use
7 Users can be authenticated in different ways. By default, Kallithea
117 the same permissions as set up via the Kallithea web
8 uses its internal user database. Alternative authentication
118 interface, you can create an authentication hook to connect
9 methods include LDAP, PAM, Crowd, and container-based authentication.
119 to the Kallithea db and run check functions for permissions
120 against that.
121
122
123 Setting up Whoosh full text search
124 ----------------------------------
125
126 Kallithea provides full text search of repositories using `Whoosh`__.
127
128 .. __: https://whoosh.readthedocs.io/en/latest/
129
130 For an incremental index build, run::
131
132 paster make-index my.ini
133
134 For a full index rebuild, run::
135
136 paster make-index my.ini -f
137
138 The ``--repo-location`` option allows the location of the repositories to be overriden;
139 usually, the location is retrieved from the Kallithea database.
140
141 The ``--index-only`` option can be used to limit the indexed repositories to a comma-separated list::
142
143 paster make-index my.ini --index-only=vcs,kallithea
144
145 To keep your index up-to-date it is necessary to do periodic index builds;
146 for this, it is recommended to use a crontab entry. Example::
147
148 0 3 * * * /path/to/virtualenv/bin/paster make-index /path/to/kallithea/my.ini
149
150 When using incremental mode (the default), Whoosh will check the last
151 modification date of each file and add it to be reindexed if a newer file is
152 available. The indexing daemon checks for any removed files and removes them
153 from index.
154
155 If you want to rebuild the index from scratch, you can use the ``-f`` flag as above,
156 or in the admin panel you can check the "build from scratch" checkbox.
157
10
158 .. _ldap-setup:
11 .. _ldap-setup:
159
12
160 Setting up LDAP support
13
161 -----------------------
14 LDAP Authentication
15 -------------------
162
16
163 Kallithea supports LDAP authentication. In order
17 Kallithea supports LDAP authentication. In order
164 to use LDAP, you have to install the python-ldap_ package. This package is
18 to use LDAP, you have to install the python-ldap_ package. This package is
@@ -178,10 +32,9 b" Here's a typical LDAP setup::"
178 Connection settings
32 Connection settings
179 Enable LDAP = checked
33 Enable LDAP = checked
180 Host = host.example.com
34 Host = host.example.com
181 Port = 389
182 Account = <account>
35 Account = <account>
183 Password = <password>
36 Password = <password>
184 Connection Security = LDAPS connection
37 Connection Security = LDAPS
185 Certificate Checks = DEMAND
38 Certificate Checks = DEMAND
186
39
187 Search settings
40 Search settings
@@ -215,8 +68,9 b' Host : required'
215
68
216 .. _Port:
69 .. _Port:
217
70
218 Port : required
71 Port : optional
219 389 for un-encrypted LDAP, 636 for SSL-encrypted LDAP.
72 Defaults to 389 for PLAIN un-encrypted LDAP and START_TLS.
73 Defaults to 636 for LDAPS.
220
74
221 .. _ldap_account:
75 .. _ldap_account:
222
76
@@ -236,26 +90,27 b' Password : optional'
236 Connection Security : required
90 Connection Security : required
237 Defines the connection to LDAP server
91 Defines the connection to LDAP server
238
92
239 No encryption
93 PLAIN
240 Plain non encrypted connection
94 Plain unencrypted LDAP connection.
95 This will by default use `Port`_ 389.
241
96
242 LDAPS connection
97 LDAPS
243 Enable LDAPS connections. It will likely require `Port`_ to be set to
98 Use secure LDAPS connections according to `Certificate
244 a different value (standard LDAPS port is 636). When LDAPS is enabled
99 Checks`_ configuration.
245 then `Certificate Checks`_ is required.
100 This will by default use `Port`_ 636.
246
101
247 START_TLS on LDAP connection
102 START_TLS
248 START TLS connection
103 Use START TLS according to `Certificate Checks`_ configuration on an
104 apparently "plain" LDAP connection.
105 This will by default use `Port`_ 389.
249
106
250 .. _Certificate Checks:
107 .. _Certificate Checks:
251
108
252 Certificate Checks : optional
109 Certificate Checks : optional
253 How SSL certificates verification is handled -- this is only useful when
110 How SSL certificates verification is handled -- this is only useful when
254 `Enable LDAPS`_ is enabled. Only DEMAND or HARD offer full SSL security
111 `Enable LDAPS`_ is enabled. Only DEMAND or HARD offer full SSL security
255 while the other options are susceptible to man-in-the-middle attacks. SSL
112 with mandatory certificate validation, while the other options are
256 certificates can be installed to /etc/openldap/cacerts so that the
113 susceptible to man-in-the-middle attacks.
257 DEMAND or HARD options can be used with self-signed certificates or
258 certificates that do not have traceable certificates of authority.
259
114
260 NEVER
115 NEVER
261 A serve certificate will never be requested or checked.
116 A serve certificate will never be requested or checked.
@@ -277,6 +132,16 b' Certificate Checks : optional'
277 HARD
132 HARD
278 The same as DEMAND.
133 The same as DEMAND.
279
134
135 .. _Custom CA Certificates:
136
137 Custom CA Certificates : optional
138 Directory used by OpenSSL to find CAs for validating the LDAP server certificate.
139 Python 2.7.10 and later default to using the system certificate store, and
140 this should thus not be necessary when using certificates signed by a CA
141 trusted by the system.
142 It can be set to something like `/etc/openldap/cacerts` on older systems or
143 if using self-signed certificates.
144
280 .. _Base DN:
145 .. _Base DN:
281
146
282 Base DN : required
147 Base DN : required
@@ -347,7 +212,7 b' information check out the Kallithea logs'
347 will be saved there.
212 will be saved there.
348
213
349 Active Directory
214 Active Directory
350 ''''''''''''''''
215 ^^^^^^^^^^^^^^^^
351
216
352 Kallithea can use Microsoft Active Directory for user authentication. This
217 Kallithea can use Microsoft Active Directory for user authentication. This
353 is done through an LDAP or LDAPS connection to Active Directory. The
218 is done through an LDAP or LDAPS connection to Active Directory. The
@@ -384,24 +249,24 b" It's also possible for an administrator "
384 permissions before the user logs in for the first time, using the :ref:`create-user` API.
249 permissions before the user logs in for the first time, using the :ref:`create-user` API.
385
250
386 Container-based authentication
251 Container-based authentication
387 ''''''''''''''''''''''''''''''
252 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
388
253
389 In a container-based authentication setup, Kallithea reads the user name from
254 In a container-based authentication setup, Kallithea reads the user name from
390 the ``REMOTE_USER`` server variable provided by the WSGI container.
255 the ``REMOTE_USER`` server variable provided by the WSGI container.
391
256
392 After setting up your container (see `Apache with mod_wsgi`_), you'll need
257 After setting up your container (see :ref:`apache_mod_wsgi`), you'll need
393 to configure it to require authentication on the location configured for
258 to configure it to require authentication on the location configured for
394 Kallithea.
259 Kallithea.
395
260
396 Proxy pass-through authentication
261 Proxy pass-through authentication
397 '''''''''''''''''''''''''''''''''
262 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
398
263
399 In a proxy pass-through authentication setup, Kallithea reads the user name
264 In a proxy pass-through authentication setup, Kallithea reads the user name
400 from the ``X-Forwarded-User`` request header, which should be configured to be
265 from the ``X-Forwarded-User`` request header, which should be configured to be
401 sent by the reverse-proxy server.
266 sent by the reverse-proxy server.
402
267
403 After setting up your proxy solution (see `Apache virtual host reverse proxy example`_,
268 After setting up your proxy solution (see :ref:`apache_virtual_host_reverse_proxy`,
404 `Apache as subdirectory`_ or `Nginx virtual host example`_), you'll need to
269 :ref:`apache_subdirectory` or :ref:`nginx_virtual_host`), you'll need to
405 configure the authentication and add the username in a request header named
270 configure the authentication and add the username in a request header named
406 ``X-Forwarded-User``.
271 ``X-Forwarded-User``.
407
272
@@ -428,6 +293,73 b' reverse-proxy setup with basic auth:'
428 RequestHeader set X-Forwarded-User %{RU}e
293 RequestHeader set X-Forwarded-User %{RU}e
429 </Location>
294 </Location>
430
295
296 Setting metadata in container/reverse-proxy
297 """""""""""""""""""""""""""""""""""""""""""
298 When a new user account is created on the first login, Kallithea has no information about
299 the user's email and full name. So you can set some additional request headers like in the
300 example below. In this example the user is authenticated via Kerberos and an Apache
301 mod_python fixup handler is used to get the user information from a LDAP server. But you
302 could set the request headers however you want.
303
304 .. code-block:: apache
305
306 <Location /someprefix>
307 ProxyPass http://127.0.0.1:5000/someprefix
308 ProxyPassReverse http://127.0.0.1:5000/someprefix
309 SetEnvIf X-Url-Scheme https HTTPS=1
310
311 AuthName "Kerberos Login"
312 AuthType Kerberos
313 Krb5Keytab /etc/apache2/http.keytab
314 KrbMethodK5Passwd off
315 KrbVerifyKDC on
316 Require valid-user
317
318 PythonFixupHandler ldapmetadata
319
320 RequestHeader set X_REMOTE_USER %{X_REMOTE_USER}e
321 RequestHeader set X_REMOTE_EMAIL %{X_REMOTE_EMAIL}e
322 RequestHeader set X_REMOTE_FIRSTNAME %{X_REMOTE_FIRSTNAME}e
323 RequestHeader set X_REMOTE_LASTNAME %{X_REMOTE_LASTNAME}e
324 </Location>
325
326 .. code-block:: python
327
328 from mod_python import apache
329 import ldap
330
331 LDAP_SERVER = "ldaps://server.mydomain.com:636"
332 LDAP_USER = ""
333 LDAP_PASS = ""
334 LDAP_ROOT = "dc=mydomain,dc=com"
335 LDAP_FILTER = "sAMAccountName=%s"
336 LDAP_ATTR_LIST = ['sAMAccountName','givenname','sn','mail']
337
338 def fixuphandler(req):
339 if req.user is None:
340 # no user to search for
341 return apache.OK
342 else:
343 try:
344 if('\\' in req.user):
345 username = req.user.split('\\')[1]
346 elif('@' in req.user):
347 username = req.user.split('@')[0]
348 else:
349 username = req.user
350 l = ldap.initialize(LDAP_SERVER)
351 l.simple_bind_s(LDAP_USER, LDAP_PASS)
352 r = l.search_s(LDAP_ROOT, ldap.SCOPE_SUBTREE, LDAP_FILTER % username, attrlist=LDAP_ATTR_LIST)
353
354 req.subprocess_env['X_REMOTE_USER'] = username
355 req.subprocess_env['X_REMOTE_EMAIL'] = r[0][1]['mail'][0].lower()
356 req.subprocess_env['X_REMOTE_FIRSTNAME'] = "%s" % r[0][1]['givenname'][0]
357 req.subprocess_env['X_REMOTE_LASTNAME'] = "%s" % r[0][1]['sn'][0]
358 except Exception, e:
359 apache.log_error("error getting data from ldap %s" % str(e), apache.APLOG_ERR)
360
361 return apache.OK
362
431 .. note::
363 .. note::
432 If you enable proxy pass-through authentication, make sure your server is
364 If you enable proxy pass-through authentication, make sure your server is
433 only accessible through the proxy. Otherwise, any client would be able to
365 only accessible through the proxy. Otherwise, any client would be able to
@@ -435,384 +367,4 b' reverse-proxy setup with basic auth:'
435 using any account of their liking.
367 using any account of their liking.
436
368
437
369
438 Integration with issue trackers
439 -------------------------------
440
441 Kallithea provides a simple integration with issue trackers. It's possible
442 to define a regular expression that will match an issue ID in commit messages,
443 and have that replaced with a URL to the issue. To enable this simply
444 uncomment the following variables in the ini file::
445
446 issue_pat = (?:^#|\s#)(\w+)
447 issue_server_link = https://issues.example.com/{repo}/issue/{id}
448 issue_prefix = #
449
450 ``issue_pat`` is the regular expression describing which strings in
451 commit messages will be treated as issue references. A match group in
452 parentheses should be used to specify the actual issue id.
453
454 The default expression matches issues in the format ``#<number>``, e.g., ``#300``.
455
456 Matched issue references are replaced with the link specified in
457 ``issue_server_link``. ``{id}`` is replaced with the issue ID, and
458 ``{repo}`` with the repository name. Since the # is stripped away,
459 ``issue_prefix`` is prepended to the link text. ``issue_prefix`` doesn't
460 necessarily need to be ``#``: if you set issue prefix to ``ISSUE-`` this will
461 generate a URL in the format:
462
463 .. code-block:: html
464
465 <a href="https://issues.example.com/example_repo/issue/300">ISSUE-300</a>
466
467 If needed, more than one pattern can be specified by appending a unique suffix to
468 the variables. For example::
469
470 issue_pat_wiki = (?:wiki-)(.+)
471 issue_server_link_wiki = https://wiki.example.com/{id}
472 issue_prefix_wiki = WIKI-
473
474 With these settings, wiki pages can be referenced as wiki-some-id, and every
475 such reference will be transformed into:
476
477 .. code-block:: html
478
479 <a href="https://wiki.example.com/some-id">WIKI-some-id</a>
480
481
482 Hook management
483 ---------------
484
485 Hooks can be managed in similar way to that used in ``.hgrc`` files.
486 To manage hooks, choose *Admin > Settings > Hooks*.
487
488 The built-in hooks cannot be modified, though they can be enabled or disabled in the *VCS* section.
489
490 To add another custom hook simply fill in the first textbox with
491 ``<name>.<hook_type>`` and the second with the hook path. Example hooks
492 can be found in ``kallithea.lib.hooks``.
493
494
495 Changing default encoding
496 -------------------------
497
498 By default, Kallithea uses UTF-8 encoding.
499 This is configurable as ``default_encoding`` in the .ini file.
500 This affects many parts in Kallithea including user names, filenames, and
501 encoding of commit messages. In addition Kallithea can detect if the ``chardet``
502 library is installed. If ``chardet`` is detected Kallithea will fallback to it
503 when there are encode/decode errors.
504
505
506 Celery configuration
507 --------------------
508
509 Kallithea can use the distributed task queue system Celery_ to run tasks like
510 cloning repositories or sending emails.
511
512 Kallithea will in most setups work perfectly fine out of the box (without
513 Celery), executing all tasks in the web server process. Some tasks can however
514 take some time to run and it can be better to run such tasks asynchronously in
515 a separate process so the web server can focus on serving web requests.
516
517 For installation and configuration of Celery, see the `Celery documentation`_.
518 Note that Celery requires a message broker service like RabbitMQ_ (recommended)
519 or Redis_.
520
521 The use of Celery is configured in the Kallithea ini configuration file.
522 To enable it, simply set::
523
524 use_celery = true
525
526 and add or change the ``celery.*`` and ``broker.*`` configuration variables.
527
528 Remember that the ini files use the format with '.' and not with '_' like
529 Celery. So for example setting `BROKER_HOST` in Celery means setting
530 `broker.host` in the configuration file.
531
532 To start the Celery process, run::
533
534 paster celeryd <configfile.ini>
535
536 .. note::
537 Make sure you run this command from the same virtualenv, and with the same
538 user that Kallithea runs.
539
540
541 HTTPS support
542 -------------
543
544 Kallithea will by default generate URLs based on the WSGI environment.
545
546 Alternatively, you can use some special configuration settings to control
547 directly which scheme/protocol Kallithea will use when generating URLs:
548
549 - With ``https_fixup = true``, the scheme will be taken from the
550 ``X-Url-Scheme``, ``X-Forwarded-Scheme`` or ``X-Forwarded-Proto`` HTTP header
551 (default ``http``).
552 - With ``force_https = true`` the default will be ``https``.
553 - With ``use_htsts = true``, Kallithea will set ``Strict-Transport-Security`` when using https.
554
555
556 Nginx virtual host example
557 --------------------------
558
559 Sample config for Nginx using proxy:
560
561 .. code-block:: nginx
562
563 upstream kallithea {
564 server 127.0.0.1:5000;
565 # add more instances for load balancing
566 #server 127.0.0.1:5001;
567 #server 127.0.0.1:5002;
568 }
569
570 ## gist alias
571 server {
572 listen 443;
573 server_name gist.example.com;
574 access_log /var/log/nginx/gist.access.log;
575 error_log /var/log/nginx/gist.error.log;
576
577 ssl on;
578 ssl_certificate gist.your.kallithea.server.crt;
579 ssl_certificate_key gist.your.kallithea.server.key;
580
581 ssl_session_timeout 5m;
582
583 ssl_protocols SSLv3 TLSv1;
584 ssl_ciphers DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:EDH-RSA-DES-CBC3-SHA:AES256-SHA:DES-CBC3-SHA:AES128-SHA:RC4-SHA:RC4-MD5;
585 ssl_prefer_server_ciphers on;
586
587 rewrite ^/(.+)$ https://kallithea.example.com/_admin/gists/$1;
588 rewrite (.*) https://kallithea.example.com/_admin/gists;
589 }
590
591 server {
592 listen 443;
593 server_name kallithea.example.com
594 access_log /var/log/nginx/kallithea.access.log;
595 error_log /var/log/nginx/kallithea.error.log;
596
597 ssl on;
598 ssl_certificate your.kallithea.server.crt;
599 ssl_certificate_key your.kallithea.server.key;
600
601 ssl_session_timeout 5m;
602
603 ssl_protocols SSLv3 TLSv1;
604 ssl_ciphers DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:EDH-RSA-DES-CBC3-SHA:AES256-SHA:DES-CBC3-SHA:AES128-SHA:RC4-SHA:RC4-MD5;
605 ssl_prefer_server_ciphers on;
606
607 ## uncomment root directive if you want to serve static files by nginx
608 ## requires static_files = false in .ini file
609 #root /path/to/installation/kallithea/public;
610 include /etc/nginx/proxy.conf;
611 location / {
612 try_files $uri @kallithea;
613 }
614
615 location @kallithea {
616 proxy_pass http://127.0.0.1:5000;
617 }
618
619 }
620
621 Here's the proxy.conf. It's tuned so it will not timeout on long
622 pushes or large pushes::
623
624 proxy_redirect off;
625 proxy_set_header Host $host;
626 ## needed for container auth
627 #proxy_set_header REMOTE_USER $remote_user;
628 #proxy_set_header X-Forwarded-User $remote_user;
629 proxy_set_header X-Url-Scheme $scheme;
630 proxy_set_header X-Host $http_host;
631 proxy_set_header X-Real-IP $remote_addr;
632 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
633 proxy_set_header Proxy-host $proxy_host;
634 proxy_buffering off;
635 proxy_connect_timeout 7200;
636 proxy_send_timeout 7200;
637 proxy_read_timeout 7200;
638 proxy_buffers 8 32k;
639 client_max_body_size 1024m;
640 client_body_buffer_size 128k;
641 large_client_header_buffers 8 64k;
642
643
644 Apache virtual host reverse proxy example
645 -----------------------------------------
646
647 Here is a sample configuration file for Apache using proxy:
648
649 .. code-block:: apache
650
651 <VirtualHost *:80>
652 ServerName kallithea.example.com
653
654 <Proxy *>
655 # For Apache 2.4 and later:
656 Require all granted
657
658 # For Apache 2.2 and earlier, instead use:
659 # Order allow,deny
660 # Allow from all
661 </Proxy>
662
663 #important !
664 #Directive to properly generate url (clone url) for pylons
665 ProxyPreserveHost On
666
667 #kallithea instance
668 ProxyPass / http://127.0.0.1:5000/
669 ProxyPassReverse / http://127.0.0.1:5000/
670
671 #to enable https use line below
672 #SetEnvIf X-Url-Scheme https HTTPS=1
673 </VirtualHost>
674
675 Additional tutorial
676 http://pylonsbook.com/en/1.1/deployment.html#using-apache-to-proxy-requests-to-pylons
677
678
679 Apache as subdirectory
680 ----------------------
681
682 Apache subdirectory part:
683
684 .. code-block:: apache
685
686 <Location /<someprefix> >
687 ProxyPass http://127.0.0.1:5000/<someprefix>
688 ProxyPassReverse http://127.0.0.1:5000/<someprefix>
689 SetEnvIf X-Url-Scheme https HTTPS=1
690 </Location>
691
692 Besides the regular apache setup you will need to add the following line
693 into ``[app:main]`` section of your .ini file::
694
695 filter-with = proxy-prefix
696
697 Add the following at the end of the .ini file::
698
699 [filter:proxy-prefix]
700 use = egg:PasteDeploy#prefix
701 prefix = /<someprefix>
702
703 then change ``<someprefix>`` into your chosen prefix
704
705
706 Apache with mod_wsgi
707 --------------------
708
709 Alternatively, Kallithea can be set up with Apache under mod_wsgi. For
710 that, you'll need to:
711
712 - Install mod_wsgi. If using a Debian-based distro, you can install
713 the package libapache2-mod-wsgi::
714
715 aptitude install libapache2-mod-wsgi
716
717 - Enable mod_wsgi::
718
719 a2enmod wsgi
720
721 - Add global Apache configuration to tell mod_wsgi that Python only will be
722 used in the WSGI processes and shouldn't be initialized in the Apache
723 processes::
724
725 WSGIRestrictEmbedded On
726
727 - Create a wsgi dispatch script, like the one below. Make sure you
728 check that the paths correctly point to where you installed Kallithea
729 and its Python Virtual Environment.
730 - Enable the ``WSGIScriptAlias`` directive for the WSGI dispatch script,
731 as in the following example. Once again, check the paths are
732 correctly specified.
733
734 Here is a sample excerpt from an Apache Virtual Host configuration file:
735
736 .. code-block:: apache
737
738 WSGIDaemonProcess kallithea processes=5 threads=1 maximum-requests=100 \
739 python-home=/srv/kallithea/venv
740 WSGIProcessGroup kallithea
741 WSGIScriptAlias / /srv/kallithea/dispatch.wsgi
742 WSGIPassAuthorization On
743
744 Or if using a dispatcher WSGI script with proper virtualenv activation:
745
746 .. code-block:: apache
747
748 WSGIDaemonProcess kallithea processes=5 threads=1 maximum-requests=100
749 WSGIProcessGroup kallithea
750 WSGIScriptAlias / /srv/kallithea/dispatch.wsgi
751 WSGIPassAuthorization On
752
753 Apache will by default run as a special Apache user, on Linux systems
754 usually ``www-data`` or ``apache``. If you need to have the repositories
755 directory owned by a different user, use the user and group options to
756 WSGIDaemonProcess to set the name of the user and group.
757
758 .. note::
759 If running Kallithea in multiprocess mode,
760 make sure you set ``instance_id = *`` in the configuration so each process
761 gets it's own cache invalidation key.
762
763 Example WSGI dispatch script:
764
765 .. code-block:: python
766
767 import os
768 os.environ["HGENCODING"] = "UTF-8"
769 os.environ['PYTHON_EGG_CACHE'] = '/srv/kallithea/.egg-cache'
770
771 # sometimes it's needed to set the curent dir
772 os.chdir('/srv/kallithea/')
773
774 import site
775 site.addsitedir("/srv/kallithea/venv/lib/python2.7/site-packages")
776
777 ini = '/srv/kallithea/my.ini'
778 from paste.script.util.logging_config import fileConfig
779 fileConfig(ini)
780 from paste.deploy import loadapp
781 application = loadapp('config:' + ini)
782
783 Or using proper virtualenv activation:
784
785 .. code-block:: python
786
787 activate_this = '/srv/kallithea/venv/bin/activate_this.py'
788 execfile(activate_this, dict(__file__=activate_this))
789
790 import os
791 os.environ['HOME'] = '/srv/kallithea'
792
793 ini = '/srv/kallithea/kallithea.ini'
794 from paste.script.util.logging_config import fileConfig
795 fileConfig(ini)
796 from paste.deploy import loadapp
797 application = loadapp('config:' + ini)
798
799
800 Other configuration files
801 -------------------------
802
803 A number of `example init.d scripts`__ can be found in
804 the ``init.d`` directory of the Kallithea source.
805
806 .. __: https://kallithea-scm.org/repos/kallithea/files/tip/init.d/ .
807
808
809 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
810 .. _python: http://www.python.org/
811 .. _Mercurial: https://www.mercurial-scm.org/
812 .. _Celery: http://celeryproject.org/
813 .. _Celery documentation: http://docs.celeryproject.org/en/latest/getting-started/index.html
814 .. _RabbitMQ: http://www.rabbitmq.com/
815 .. _Redis: http://redis.io/
816 .. _python-ldap: http://www.python-ldap.org/
370 .. _python-ldap: http://www.python-ldap.org/
817 .. _mercurial-server: http://www.lshift.net/mercurial-server.html
818 .. _PublishingRepositories: https://www.mercurial-scm.org/wiki/PublishingRepositories
@@ -1,14 +1,15 b''
1 .. _vcs_support:
1 .. _vcs_setup:
2
2
3 ===============================
3 =============================
4 Version control systems support
4 Version control systems setup
5 ===============================
5 =============================
6
6
7 Kallithea supports Git and Mercurial repositories out-of-the-box.
7 Kallithea supports Git and Mercurial repositories out-of-the-box.
8 For Git, you do need the ``git`` command line client installed on the server.
8 For Git, you do need the ``git`` command line client installed on the server.
9
9
10 You can always disable Git or Mercurial support by editing the
10 You can always disable Git or Mercurial support by editing the
11 file ``kallithea/__init__.py`` and commenting out the backend.
11 file ``kallithea/__init__.py`` and commenting out the backend. For example, to
12 disable Git but keep Mercurial enabled:
12
13
13 .. code-block:: python
14 .. code-block:: python
14
15
@@ -18,20 +19,20 b' file ``kallithea/__init__.py`` and comme'
18 }
19 }
19
20
20
21
21 Git support
22 Git-specific setup
22 -----------
23 ------------------
23
24
24
25
25 Web server with chunked encoding
26 Web server with chunked encoding
26 ````````````````````````````````
27 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
27
28
28 Large Git pushes require an HTTP server with support for
29 Large Git pushes require an HTTP server with support for
29 chunked encoding for POST. The Python web servers waitress_ and
30 chunked encoding for POST. The Python web servers waitress_ and
30 gunicorn_ (Linux only) can be used. By default, Kallithea uses
31 gunicorn_ (Linux only) can be used. By default, Kallithea uses
31 waitress_ for `paster serve` instead of the built-in `paste` WSGI
32 waitress_ for `gearbox serve` instead of the built-in `paste` WSGI
32 server.
33 server.
33
34
34 The paster server is controlled in the .ini file::
35 The web server used by gearbox is controlled in the .ini file::
35
36
36 use = egg:waitress#main
37 use = egg:waitress#main
37
38
@@ -45,41 +46,14 b' Also make sure to comment out the follow'
45 threadpool_max_requests =
46 threadpool_max_requests =
46 use_threadpool =
47 use_threadpool =
47
48
48
49 Increasing Git HTTP POST buffer size
49 Mercurial support
50 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
50 -----------------
51
52
53 Working with Mercurial subrepositories
54 ``````````````````````````````````````
55
56 This section explains how to use Mercurial subrepositories_ in Kallithea.
57
58 Example usage::
59
51
60 ## init a simple repo
52 If Git pushes fail with HTTP error code 411 (Length Required), you may need to
61 hg init mainrepo
53 increase the Git HTTP POST buffer. Run the following command as the user that
62 cd mainrepo
54 runs Kallithea to set a global Git variable to this effect::
63 echo "file" > file
64 hg add file
65 hg ci --message "initial file"
66
67 # clone subrepo we want to add from Kallithea
68 hg clone http://kallithea.local/subrepo
69
55
70 ## specify URL to existing repo in Kallithea as subrepository path
56 git config --global http.postBuffer 524288000
71 echo "subrepo = http://kallithea.local/subrepo" > .hgsub
72 hg add .hgsub
73 hg ci --message "added remote subrepo"
74
75 In the file list of a clone of ``mainrepo`` you will see a connected
76 subrepository at the revision it was cloned with. Clicking on the
77 subrepository link sends you to the proper repository in Kallithea.
78
79 Cloning ``mainrepo`` will also clone the attached subrepository.
80
81 Next we can edit the subrepository data, and push back to Kallithea. This will
82 update both repositories.
83
57
84
58
85 .. _waitress: http://pypi.python.org/pypi/waitress
59 .. _waitress: http://pypi.python.org/pypi/waitress
@@ -9,34 +9,16 b' methods. Everything is available by send'
9 ``<your_server>/_admin/api``.
9 ``<your_server>/_admin/api``.
10
10
11
11
12 API access for web views
12 API keys
13 ++++++++++++++++++++++++
13 --------
14
15 API access can also be turned on for each web view in Kallithea that is
16 decorated with the ``@LoginRequired`` decorator. Some views use
17 ``@LoginRequired(api_access=True)`` and are always available. By default only
18 RSS/Atom feed views are enabled. Other views are
19 only available if they have been whitelisted. Edit the
20 ``api_access_controllers_whitelist`` option in your .ini file and define views
21 that should have API access enabled.
22
14
23 For example, to enable API access to patch/diff, raw file and archive::
15 Every Kallithea user automatically receives an API key, which they can
24
16 view under "My Account". On this page, API keys can also be revoked, and
25 api_access_controllers_whitelist =
17 additional API keys can be generated.
26 ChangesetController:changeset_patch,
27 ChangesetController:changeset_raw,
28 FilesController:raw,
29 FilesController:archivefile
30
31 After this change, a Kallithea view can be accessed without login by adding a
32 GET parameter ``?api_key=<api_key>`` to the URL.
33
34 Exposing raw diffs is a good way to integrate with
35 third-party services like code review, or build farms that can download archives.
36
18
37
19
38 API access
20 API access
39 ++++++++++
21 ----------
40
22
41 Clients must send JSON encoded JSON-RPC requests::
23 Clients must send JSON encoded JSON-RPC requests::
42
24
@@ -76,7 +58,7 b' the reponse will have a failure descript'
76
58
77
59
78 API client
60 API client
79 ++++++++++
61 ----------
80
62
81 Kallithea comes with a ``kallithea-api`` command line tool, providing a convenient
63 Kallithea comes with a ``kallithea-api`` command line tool, providing a convenient
82 way to call the JSON-RPC API.
64 way to call the JSON-RPC API.
@@ -110,11 +92,11 b" so you don't have to specify them every "
110
92
111
93
112 API methods
94 API methods
113 +++++++++++
95 -----------
114
96
115
97
116 pull
98 pull
117 ----
99 ^^^^
118
100
119 Pull the given repo from remote location. Can be used to automatically keep
101 Pull the given repo from remote location. Can be used to automatically keep
120 remote repos up to date.
102 remote repos up to date.
@@ -136,7 +118,7 b' OUTPUT::'
136 error : null
118 error : null
137
119
138 rescan_repos
120 rescan_repos
139 ------------
121 ^^^^^^^^^^^^
140
122
141 Rescan repositories. If ``remove_obsolete`` is set,
123 Rescan repositories. If ``remove_obsolete`` is set,
142 Kallithea will delete repos that are in the database but not in the filesystem.
124 Kallithea will delete repos that are in the database but not in the filesystem.
@@ -159,7 +141,7 b' OUTPUT::'
159 error : null
141 error : null
160
142
161 invalidate_cache
143 invalidate_cache
162 ----------------
144 ^^^^^^^^^^^^^^^^
163
145
164 Invalidate the cache for a repository.
146 Invalidate the cache for a repository.
165 This command can only be executed using the api_key of a user with admin rights,
147 This command can only be executed using the api_key of a user with admin rights,
@@ -181,7 +163,7 b' OUTPUT::'
181 error : null
163 error : null
182
164
183 lock
165 lock
184 ----
166 ^^^^
185
167
186 Set the locking state on the given repository by the given user.
168 Set the locking state on the given repository by the given user.
187 If the param ``userid`` is skipped, it is set to the ID of the user who is calling this method.
169 If the param ``userid`` is skipped, it is set to the ID of the user who is calling this method.
@@ -212,7 +194,7 b' OUTPUT::'
212 error : null
194 error : null
213
195
214 get_ip
196 get_ip
215 ------
197 ^^^^^^
216
198
217 Return IP address as seen from Kallithea server, together with all
199 Return IP address as seen from Kallithea server, together with all
218 defined IP addresses for given user.
200 defined IP addresses for given user.
@@ -244,12 +226,12 b' OUTPUT::'
244 error : null
226 error : null
245
227
246 get_user
228 get_user
247 --------
229 ^^^^^^^^
248
230
249 Get a user by username or userid. The result is empty if user can't be found.
231 Get a user by username or userid. The result is empty if user can't be found.
250 If userid param is skipped, it is set to id of user who is calling this method.
232 If userid param is skipped, it is set to id of user who is calling this method.
251 Any userid can be specified when the command is executed using the api_key of a user with admin rights.
233 Any userid can be specified when the command is executed using the api_key of a user with admin rights.
252 Regular users can only speicy their own userid.
234 Regular users can only specify their own userid.
253
235
254 INPUT::
236 INPUT::
255
237
@@ -288,7 +270,7 b' OUTPUT::'
288 error: null
270 error: null
289
271
290 get_users
272 get_users
291 ---------
273 ^^^^^^^^^
292
274
293 List all existing users.
275 List all existing users.
294 This command can only be executed using the api_key of a user with admin rights.
276 This command can only be executed using the api_key of a user with admin rights.
@@ -325,7 +307,7 b' OUTPUT::'
325 .. _create-user:
307 .. _create-user:
326
308
327 create_user
309 create_user
328 -----------
310 ^^^^^^^^^^^
329
311
330 Create new user.
312 Create new user.
331 This command can only be executed using the api_key of a user with admin rights.
313 This command can only be executed using the api_key of a user with admin rights.
@@ -371,7 +353,7 b' Example::'
371 kallithea-api create_user username:bent email:bent@example.com firstname:Bent lastname:Bentsen extern_type:ldap extern_name:uid=bent,dc=example,dc=com
353 kallithea-api create_user username:bent email:bent@example.com firstname:Bent lastname:Bentsen extern_type:ldap extern_name:uid=bent,dc=example,dc=com
372
354
373 update_user
355 update_user
374 -----------
356 ^^^^^^^^^^^
375
357
376 Update the given user if such user exists.
358 Update the given user if such user exists.
377 This command can only be executed using the api_key of a user with admin rights.
359 This command can only be executed using the api_key of a user with admin rights.
@@ -415,7 +397,7 b' OUTPUT::'
415 error: null
397 error: null
416
398
417 delete_user
399 delete_user
418 -----------
400 ^^^^^^^^^^^
419
401
420 Delete the given user if such a user exists.
402 Delete the given user if such a user exists.
421 This command can only be executed using the api_key of a user with admin rights.
403 This command can only be executed using the api_key of a user with admin rights.
@@ -439,7 +421,7 b' OUTPUT::'
439 error: null
421 error: null
440
422
441 get_user_group
423 get_user_group
442 --------------
424 ^^^^^^^^^^^^^^
443
425
444 Get an existing user group.
426 Get an existing user group.
445 This command can only be executed using the api_key of a user with admin rights.
427 This command can only be executed using the api_key of a user with admin rights.
@@ -481,7 +463,7 b' OUTPUT::'
481 error : null
463 error : null
482
464
483 get_user_groups
465 get_user_groups
484 ---------------
466 ^^^^^^^^^^^^^^^
485
467
486 List all existing user groups.
468 List all existing user groups.
487 This command can only be executed using the api_key of a user with admin rights.
469 This command can only be executed using the api_key of a user with admin rights.
@@ -507,7 +489,7 b' OUTPUT::'
507 error : null
489 error : null
508
490
509 create_user_group
491 create_user_group
510 -----------------
492 ^^^^^^^^^^^^^^^^^
511
493
512 Create a new user group.
494 Create a new user group.
513 This command can only be executed using the api_key of a user with admin rights.
495 This command can only be executed using the api_key of a user with admin rights.
@@ -537,7 +519,7 b' OUTPUT::'
537 error: null
519 error: null
538
520
539 add_user_to_user_group
521 add_user_to_user_group
540 ----------------------
522 ^^^^^^^^^^^^^^^^^^^^^^
541
523
542 Adds a user to a user group. If the user already is in that group, success will be
524 Adds a user to a user group. If the user already is in that group, success will be
543 ``false``.
525 ``false``.
@@ -564,7 +546,7 b' OUTPUT::'
564 error: null
546 error: null
565
547
566 remove_user_from_user_group
548 remove_user_from_user_group
567 ---------------------------
549 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
568
550
569 Remove a user from a user group. If the user isn't in the given group, success will
551 Remove a user from a user group. If the user isn't in the given group, success will
570 be ``false``.
552 be ``false``.
@@ -591,7 +573,7 b' OUTPUT::'
591 error: null
573 error: null
592
574
593 get_repo
575 get_repo
594 --------
576 ^^^^^^^^
595
577
596 Get an existing repository by its name or repository_id. Members will contain
578 Get an existing repository by its name or repository_id. Members will contain
597 either users_group or users associated to that repository.
579 either users_group or users associated to that repository.
@@ -604,7 +586,9 b' INPUT::'
604 api_key : "<api_key>"
586 api_key : "<api_key>"
605 method : "get_repo"
587 method : "get_repo"
606 args: {
588 args: {
607 "repoid" : "<reponame or repo_id>"
589 "repoid" : "<reponame or repo_id>",
590 "with_revision_names": "<bool> = Optional(False)",
591 "with_pullrequests": "<bool> = Optional(False)",
608 }
592 }
609
593
610 OUTPUT::
594 OUTPUT::
@@ -630,7 +614,7 b' OUTPUT::'
630 "raw_id": "<raw_id>",
614 "raw_id": "<raw_id>",
631 "revision": "<numeric_revision>",
615 "revision": "<numeric_revision>",
632 "short_id": "<short_id>"
616 "short_id": "<short_id>"
633 }
617 },
634 "owner": "<repo_owner>",
618 "owner": "<repo_owner>",
635 "fork_of": "<name_of_fork_parent>",
619 "fork_of": "<name_of_fork_parent>",
636 "members" : [
620 "members" : [
@@ -658,7 +642,7 b' OUTPUT::'
658 "permission" : "repository.(read|write|admin)"
642 "permission" : "repository.(read|write|admin)"
659 },
643 },
660
644
661 ]
645 ],
662 "followers": [
646 "followers": [
663 {
647 {
664 "user_id" : "<user_id>",
648 "user_id" : "<user_id>",
@@ -675,12 +659,74 b' OUTPUT::'
675 "last_login": "<last_login>",
659 "last_login": "<last_login>",
676 },
660 },
677
661
678 ]
662 ],
663 <if with_revision_names == True>
664 "tags": {
665 "<tagname>": "<raw_id>",
666 ...
667 },
668 "branches": {
669 "<branchname>": "<raw_id>",
670 ...
671 },
672 "bookmarks": {
673 "<bookmarkname>": "<raw_id>",
674 ...
675 },
676 <if with_pullrequests == True>
677 "pull_requests": [
678 {
679 "status": "<pull_request_status>",
680 "pull_request_id": <pull_request_id>,
681 "description": "<pull_request_description>",
682 "title": "<pull_request_title>",
683 "url": "<pull_request_url>",
684 "reviewers": [
685 {
686 "username": "<user_id>",
687 },
688 ...
689 ],
690 "org_repo_url": "<repo_url>",
691 "org_ref_parts": [
692 "<ref_type>",
693 "<ref_name>",
694 "<raw_id>"
695 ],
696 "other_ref_parts": [
697 "<ref_type>",
698 "<ref_name>",
699 "<raw_id>"
700 ],
701 "comments": [
702 {
703 "username": "<user_id>",
704 "text": "<comment text>",
705 "comment_id": "<comment_id>",
706 },
707 ...
708 ],
709 "owner": "<username>",
710 "statuses": [
711 {
712 "status": "<status_of_review>", # "under_review", "approved" or "rejected"
713 "reviewer": "<user_id>",
714 "modified_at": "<date_time_of_review>" # iso 8601 date, server's timezone
715 },
716 ...
717 ],
718 "revisions": [
719 "<raw_id>",
720 ...
721 ]
722 },
723 ...
724 ]
679 }
725 }
680 error: null
726 error: null
681
727
682 get_repos
728 get_repos
683 ---------
729 ^^^^^^^^^
684
730
685 List all existing repositories.
731 List all existing repositories.
686 This command can only be executed using the api_key of a user with admin rights,
732 This command can only be executed using the api_key of a user with admin rights,
@@ -717,7 +763,7 b' OUTPUT::'
717 error: null
763 error: null
718
764
719 get_repo_nodes
765 get_repo_nodes
720 --------------
766 ^^^^^^^^^^^^^^
721
767
722 Return a list of files and directories for a given path at the given revision.
768 Return a list of files and directories for a given path at the given revision.
723 It is possible to specify ret_type to show only ``files`` or ``dirs``.
769 It is possible to specify ret_type to show only ``files`` or ``dirs``.
@@ -748,7 +794,7 b' OUTPUT::'
748 error: null
794 error: null
749
795
750 create_repo
796 create_repo
751 -----------
797 ^^^^^^^^^^^
752
798
753 Create a repository. If the repository name contains "/", the repository will be
799 Create a repository. If the repository name contains "/", the repository will be
754 created in the repository group indicated by that path. Any such repository
800 created in the repository group indicated by that path. Any such repository
@@ -802,7 +848,7 b' OUTPUT::'
802 error: null
848 error: null
803
849
804 update_repo
850 update_repo
805 -----------
851 ^^^^^^^^^^^
806
852
807 Update a repository.
853 Update a repository.
808 This command can only be executed using the api_key of a user with admin rights,
854 This command can only be executed using the api_key of a user with admin rights,
@@ -862,7 +908,7 b' OUTPUT::'
862 error: null
908 error: null
863
909
864 fork_repo
910 fork_repo
865 ---------
911 ^^^^^^^^^
866
912
867 Create a fork of the given repo. If using Celery, this will
913 Create a fork of the given repo. If using Celery, this will
868 return success message immediately and a fork will be created
914 return success message immediately and a fork will be created
@@ -898,7 +944,7 b' OUTPUT::'
898 error: null
944 error: null
899
945
900 delete_repo
946 delete_repo
901 -----------
947 ^^^^^^^^^^^
902
948
903 Delete a repository.
949 Delete a repository.
904 This command can only be executed using the api_key of a user with admin rights,
950 This command can only be executed using the api_key of a user with admin rights,
@@ -925,7 +971,7 b' OUTPUT::'
925 error: null
971 error: null
926
972
927 grant_user_permission
973 grant_user_permission
928 ---------------------
974 ^^^^^^^^^^^^^^^^^^^^^
929
975
930 Grant permission for a user on the given repository, or update the existing one if found.
976 Grant permission for a user on the given repository, or update the existing one if found.
931 This command can only be executed using the api_key of a user with admin rights.
977 This command can only be executed using the api_key of a user with admin rights.
@@ -951,7 +997,7 b' OUTPUT::'
951 error: null
997 error: null
952
998
953 revoke_user_permission
999 revoke_user_permission
954 ----------------------
1000 ^^^^^^^^^^^^^^^^^^^^^^
955
1001
956 Revoke permission for a user on the given repository.
1002 Revoke permission for a user on the given repository.
957 This command can only be executed using the api_key of a user with admin rights.
1003 This command can only be executed using the api_key of a user with admin rights.
@@ -976,7 +1022,7 b' OUTPUT::'
976 error: null
1022 error: null
977
1023
978 grant_user_group_permission
1024 grant_user_group_permission
979 ---------------------------
1025 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
980
1026
981 Grant permission for a user group on the given repository, or update the
1027 Grant permission for a user group on the given repository, or update the
982 existing one if found.
1028 existing one if found.
@@ -1003,7 +1049,7 b' OUTPUT::'
1003 error: null
1049 error: null
1004
1050
1005 revoke_user_group_permission
1051 revoke_user_group_permission
1006 ----------------------------
1052 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1007
1053
1008 Revoke permission for a user group on the given repository.
1054 Revoke permission for a user group on the given repository.
1009 This command can only be executed using the api_key of a user with admin rights.
1055 This command can only be executed using the api_key of a user with admin rights.
@@ -1026,3 +1072,235 b' OUTPUT::'
1026 "success": true
1072 "success": true
1027 }
1073 }
1028 error: null
1074 error: null
1075
1076 get_changesets
1077 ^^^^^^^^^^^^^^
1078
1079 Get changesets of a given repository. This command can only be executed using the api_key
1080 of a user with read permissions to the repository.
1081
1082 INPUT::
1083
1084 id : <id_for_response>
1085 api_key : "<api_key>"
1086 method : "get_changesets"
1087 args: {
1088 "repoid" : "<reponame or repo_id>",
1089 "start": "<revision number> = Optional(None)",
1090 "end": "<revision number> = Optional(None)",
1091 "start_date": "<date> = Optional(None)", # in "%Y-%m-%dT%H:%M:%S" format
1092 "end_date": "<date> = Optional(None)", # in "%Y-%m-%dT%H:%M:%S" format
1093 "branch_name": "<branch name filter> = Optional(None)",
1094 "reverse": "<bool> = Optional(False)",
1095 "with_file_list": "<bool> = Optional(False)"
1096 }
1097
1098 OUTPUT::
1099
1100 id : <id_given_in_input>
1101 result: [
1102 {
1103 "raw_id": "<raw_id>",
1104 "short_id": "short_id": "<short_id>",
1105 "author": "<full_author>",
1106 "date": "<date_time_of_commit>",
1107 "message": "<commit_message>",
1108 "revision": "<numeric_revision>",
1109 <if with_file_list == True>
1110 "added": [<list of added files>],
1111 "changed": [<list of changed files>],
1112 "removed": [<list of removed files>]
1113 },
1114 ...
1115 ]
1116 error: null
1117
1118 get_changeset
1119 ^^^^^^^^^^^^^
1120
1121 Get information and review status for a given changeset. This command can only
1122 be executed using the api_key of a user with read permissions to the
1123 repository.
1124
1125 INPUT::
1126
1127 id : <id_for_response>
1128 api_key : "<api_key>"
1129 method : "get_changeset"
1130 args: {
1131 "repoid" : "<reponame or repo_id>",
1132 "raw_id" : "<raw_id>",
1133 "with_reviews": "<bool> = Optional(False)"
1134 }
1135
1136 OUTPUT::
1137
1138 id : <id_given_in_input>
1139 result: {
1140 "author": "<full_author>",
1141 "date": "<date_time_of_commit>",
1142 "message": "<commit_message>",
1143 "raw_id": "<raw_id>",
1144 "revision": "<numeric_revision>",
1145 "short_id": "<short_id>",
1146 "reviews": [{
1147 "reviewer": "<username>",
1148 "modified_at": "<date_time_of_review>", # iso 8601 date, server's timezone
1149 "status": "<status_of_review>", # "under_review", "approved" or "rejected"
1150 },
1151 ...
1152 ]
1153 }
1154 error: null
1155
1156 Example output::
1157
1158 {
1159 "id" : 1,
1160 "error" : null,
1161 "result" : {
1162 "author" : {
1163 "email" : "user@example.com",
1164 "name" : "Kallithea Admin"
1165 },
1166 "changed" : [],
1167 "short_id" : "e1022d3d28df",
1168 "date" : "2017-03-28T09:09:03",
1169 "added" : [
1170 "README.rst"
1171 ],
1172 "removed" : [],
1173 "revision" : 0,
1174 "raw_id" : "e1022d3d28dfba02f626cde65dbe08f4ceb0e4e7",
1175 "message" : "Added file via Kallithea",
1176 "id" : "e1022d3d28dfba02f626cde65dbe08f4ceb0e4e7",
1177 "reviews" : [
1178 {
1179 "status" : "under_review",
1180 "modified_at" : "2017-03-28T09:17:08.618",
1181 "reviewer" : "user"
1182 }
1183 ]
1184 }
1185 }
1186
1187 get_pullrequest
1188 ^^^^^^^^^^^^^^^
1189
1190 Get information and review status for a given pull request. This command can only be executed
1191 using the api_key of a user with read permissions to the original repository.
1192
1193 INPUT::
1194
1195 id : <id_for_response>
1196 api_key : "<api_key>"
1197 method : "get_pullrequest"
1198 args: {
1199 "pullrequest_id" : "<pullrequest_id>",
1200 }
1201
1202 OUTPUT::
1203
1204 id : <id_given_in_input>
1205 result: {
1206 "status": "<pull_request_status>",
1207 "pull_request_id": <pull_request_id>,
1208 "description": "<pull_request_description>",
1209 "title": "<pull_request_title>",
1210 "url": "<pull_request_url>",
1211 "reviewers": [
1212 {
1213 "username": "<user_name>",
1214 },
1215 ...
1216 ],
1217 "org_repo_url": "<repo_url>",
1218 "org_ref_parts": [
1219 "<ref_type>",
1220 "<ref_name>",
1221 "<raw_id>"
1222 ],
1223 "other_ref_parts": [
1224 "<ref_type>",
1225 "<ref_name>",
1226 "<raw_id>"
1227 ],
1228 "comments": [
1229 {
1230 "username": "<user_name>",
1231 "text": "<comment text>",
1232 "comment_id": "<comment_id>",
1233 },
1234 ...
1235 ],
1236 "owner": "<username>",
1237 "statuses": [
1238 {
1239 "status": "<status_of_review>", # "under_review", "approved" or "rejected"
1240 "reviewer": "<user_name>",
1241 "modified_at": "<date_time_of_review>" # iso 8601 date, server's timezone
1242 },
1243 ...
1244 ],
1245 "revisions": [
1246 "<raw_id>",
1247 ...
1248 ]
1249 },
1250 error: null
1251
1252 comment_pullrequest
1253 ^^^^^^^^^^^^^^^^^^^
1254
1255 Add comment, change status or close a given pull request. This command can only be executed
1256 using the api_key of a user with read permissions to the original repository.
1257
1258 INPUT::
1259
1260 id : <id_for_response>
1261 api_key : "<api_key>"
1262 method : "comment_pullrequest"
1263 args: {
1264 "pull_request_id": "<pull_request_id>",
1265 "comment_msg": Optional(''),
1266 "status": Optional(None), # "under_review", "approved" or "rejected"
1267 "close_pr": Optional(False)",
1268 }
1269
1270 OUTPUT::
1271
1272 id : <id_given_in_input>
1273 result: True
1274 error: null
1275
1276
1277 API access for web views
1278 ------------------------
1279
1280 API access can also be turned on for each web view in Kallithea that is
1281 decorated with the ``@LoginRequired`` decorator. Some views use
1282 ``@LoginRequired(api_access=True)`` and are always available. By default only
1283 RSS/Atom feed views are enabled. Other views are
1284 only available if they have been whitelisted. Edit the
1285 ``api_access_controllers_whitelist`` option in your .ini file and define views
1286 that should have API access enabled.
1287
1288 For example, to enable API access to patch/diff, raw file and archive::
1289
1290 api_access_controllers_whitelist =
1291 ChangesetController:changeset_patch,
1292 ChangesetController:changeset_raw,
1293 FilesController:raw,
1294 FilesController:archivefile
1295
1296 After this change, a Kallithea view can be accessed without login using
1297 bearer authentication, by including this header with the request::
1298
1299 Authentication: Bearer <api_key>
1300
1301 Alternatively, the API key can be passed in the URL query string using
1302 ``?api_key=<api_key>``, though this is not recommended due to the increased
1303 risk of API key leaks, and support will likely be removed in the future.
1304
1305 Exposing raw diffs is a good way to integrate with
1306 third-party services like code review, or build farms that can download archives.
@@ -10,9 +10,6 b' The :mod:`models` module'
10 .. automodule:: kallithea.model.comment
10 .. automodule:: kallithea.model.comment
11 :members:
11 :members:
12
12
13 .. automodule:: kallithea.model.notification
14 :members:
15
16 .. automodule:: kallithea.model.permission
13 .. automodule:: kallithea.model.permission
17 :members:
14 :members:
18
15
@@ -28,62 +28,179 b' for more details.'
28 Getting started
28 Getting started
29 ---------------
29 ---------------
30
30
31 To get started with development::
31 To get started with Kallithea development::
32
32
33 hg clone https://kallithea-scm.org/repos/kallithea
33 hg clone https://kallithea-scm.org/repos/kallithea
34 cd kallithea
34 cd kallithea
35 virtualenv ../kallithea-venv
35 virtualenv ../kallithea-venv
36 source ../kallithea-venv/bin/activate
36 source ../kallithea-venv/bin/activate
37 pip install --upgrade pip "setuptools<34"
37 pip install --upgrade pip setuptools
38 pip install -e .
38 pip install --upgrade -e .
39 paster make-config Kallithea my.ini
39 pip install --upgrade -r dev_requirements.txt
40 paster setup-db my.ini --user=user --email=user@example.com --password=password --repos=/tmp
40 kallithea-cli config-create my.ini
41 paster serve my.ini --reload &
41 kallithea-cli db-create -c my.ini --user=user --email=user@example.com --password=password --repos=/tmp
42 kallithea-cli front-end-build
43 gearbox serve -c my.ini --reload &
42 firefox http://127.0.0.1:5000/
44 firefox http://127.0.0.1:5000/
43
45
44 You can also start out by forking https://bitbucket.org/conservancy/kallithea
46 If you plan to use Bitbucket_ for sending contributions, you can also fork
45 on Bitbucket_ and create a local clone of your own fork.
47 Kallithea on Bitbucket_ first (https://bitbucket.org/conservancy/kallithea) and
48 then replace the clone step above by a clone of your fork. In this case, please
49 see :ref:`contributing-guidelines` below for configuring your fork correctly.
50
51
52 Contribution flow
53 -----------------
54
55 Starting from an existing Kallithea clone, make sure it is up to date with the
56 latest upstream changes::
57
58 hg pull
59 hg update
60
61 Review the :ref:`contributing-guidelines` and :ref:`coding-guidelines`.
62
63 If you are new to Mercurial, refer to Mercurial `Quick Start`_ and `Beginners
64 Guide`_ on the Mercurial wiki.
65
66 Now, make some changes and test them (see :ref:`contributing-tests`). Don't
67 forget to add new tests to cover new functionality or bug fixes.
68
69 For documentation changes, run ``make html`` from the ``docs`` directory to
70 generate the HTML result, then review them in your browser.
71
72 Before submitting any changes, run the cleanup script::
73
74 ./scripts/run-all-cleanup
75
76 When you are completely ready, you can send your changes to the community for
77 review and inclusion. Most commonly used methods are sending patches to the
78 mailing list (via ``hg email``) or by creating a pull request on Bitbucket_.
79
80 .. _contributing-tests:
46
81
47
82
48 Running tests
83 Running tests
49 -------------
84 -------------
50
85
51 After finishing your changes make sure all tests pass cleanly. You can run
86 After finishing your changes make sure all tests pass cleanly. Run the testsuite
52 the testsuite running ``nosetests`` from the project root, or if you use tox
87 by invoking ``py.test`` from the project root::
53 run ``tox`` for Python 2.6--2.7 with multiple database test.
88
89 py.test
90
91 Note that testing on Python 2.6 also requires ``unittest2``.
54
92
55 When running tests, Kallithea uses `kallithea/tests/test.ini` and populates the
93 Note that on unix systems, the temporary directory (``/tmp`` or where
56 SQLite database specified there.
94 ``$TMPDIR`` points) must allow executable files; Git hooks must be executable,
95 and the test suite creates repositories in the temporary directory. Linux
96 systems with /tmp mounted noexec will thus fail.
97
98 You can also use ``tox`` to run the tests with all supported Python versions
99 (currently Python 2.6--2.7).
100
101 When running tests, Kallithea generates a `test.ini` based on template values
102 in `kallithea/tests/conftest.py` and populates the SQLite database specified
103 there.
57
104
58 It is possible to avoid recreating the full test database on each invocation of
105 It is possible to avoid recreating the full test database on each invocation of
59 the tests, thus eliminating the initial delay. To achieve this, run the tests as::
106 the tests, thus eliminating the initial delay. To achieve this, run the tests as::
60
107
61 paster serve kallithea/tests/test.ini --pid-file=test.pid --daemon
108 gearbox serve -c /tmp/kallithea-test-XXX/test.ini --pid-file=test.pid --daemon
62 KALLITHEA_WHOOSH_TEST_DISABLE=1 KALLITHEA_NO_TMP_PATH=1 nosetests
109 KALLITHEA_WHOOSH_TEST_DISABLE=1 KALLITHEA_NO_TMP_PATH=1 py.test
63 kill -9 $(cat test.pid)
110 kill -9 $(cat test.pid)
64
111
65 You can run individual tests by specifying their path as argument to nosetests.
112 In these commands, the following variables are used::
66 nosetests also has many more options, see `nosetests -h`. Some useful options
113
114 KALLITHEA_WHOOSH_TEST_DISABLE=1 - skip whoosh index building and tests
115 KALLITHEA_NO_TMP_PATH=1 - disable new temp path for tests, used mostly for testing_vcs_operations
116
117 You can run individual tests by specifying their path as argument to py.test.
118 py.test also has many more options, see `py.test -h`. Some useful options
67 are::
119 are::
68
120
69 -x, --stop Stop running tests after the first error or failure
121 -k EXPRESSION only run tests which match the given substring
70 -s, --nocapture Don't capture stdout (any stdout output will be
122 expression. An expression is a python evaluable
71 printed immediately) [NOSE_NOCAPTURE]
123 expression where all names are substring-matched
72 --failed Run the tests that failed in the last test run.
124 against test names and their parent classes. Example:
125 -x, --exitfirst exit instantly on first error or failed test.
126 --lf rerun only the tests that failed at the last run (or
127 all if none failed)
128 --ff run all tests but run the last failures first. This
129 may re-order tests and thus lead to repeated fixture
130 setup/teardown
131 --pdb start the interactive Python debugger on errors.
132 -s, --capture=no don't capture stdout (any stdout output will be
133 printed immediately)
134
135 Performance tests
136 ^^^^^^^^^^^^^^^^^
137
138 A number of performance tests are present in the test suite, but they are
139 not run in a standard test run. These tests are useful to
140 evaluate the impact of certain code changes with respect to performance.
141
142 To run these tests::
143
144 env TEST_PERFORMANCE=1 py.test kallithea/tests/performance
145
146 To analyze performance, you could install pytest-profiling_, which enables the
147 --profile and --profile-svg options to py.test.
148
149 .. _pytest-profiling: https://github.com/manahl/pytest-plugins/tree/master/pytest-profiling
150
151 .. _contributing-guidelines:
73
152
74
153
75 Coding/contribution guidelines
154 Contribution guidelines
76 ------------------------------
155 -----------------------
77
156
78 Kallithea is GPLv3 and we assume all contributions are made by the
157 Kallithea is GPLv3 and we assume all contributions are made by the
79 committer/contributor and under GPLv3 unless explicitly stated. We do care a
158 committer/contributor and under GPLv3 unless explicitly stated. We do care a
80 lot about preservation of copyright and license information for existing code
159 lot about preservation of copyright and license information for existing code
81 that is brought into the project.
160 that is brought into the project.
82
161
162 Contributions will be accepted in most formats -- such as pull requests on
163 Bitbucket, something hosted on your own Kallithea instance, or patches sent by
164 email to the `kallithea-general`_ mailing list.
165
166 When contributing via Bitbucket, please make your fork of
167 https://bitbucket.org/conservancy/kallithea/ `non-publishing`_ -- it is one of
168 the settings on "Repository details" page. This ensures your commits are in
169 "draft" phase and makes it easier for you to address feedback and for project
170 maintainers to integrate your changes.
171
172 .. _non-publishing: https://www.mercurial-scm.org/wiki/Phases#Publishing_Repository
173
174 Make sure to test your changes both manually and with the automatic tests
175 before posting.
176
177 We care about quality and review and keeping a clean repository history. We
178 might give feedback that requests polishing contributions until they are
179 "perfect". We might also rebase and collapse and make minor adjustments to your
180 changes when we apply them.
181
182 We try to make sure we have consensus on the direction the project is taking.
183 Everything non-sensitive should be discussed in public -- preferably on the
184 mailing list. We aim at having all non-trivial changes reviewed by at least
185 one other core developer before pushing. Obvious non-controversial changes will
186 be handled more casually.
187
188 There is a main development branch ("default") which is generally stable so that
189 it can be (and is) used in production. There is also a "stable" branch that is
190 almost exclusively reserved for bug fixes or trivial changes. Experimental
191 changes should live elsewhere (for example in a pull request) until they are
192 ready.
193
194 .. _coding-guidelines:
195
196
197 Coding guidelines
198 -----------------
199
83 We don't have a formal coding/formatting standard. We are currently using a mix
200 We don't have a formal coding/formatting standard. We are currently using a mix
84 of Mercurial's (https://www.mercurial-scm.org/wiki/CodingStyle), pep8, and
201 of Mercurial's (https://www.mercurial-scm.org/wiki/CodingStyle), pep8, and
85 consistency with existing code. Run whitespacecleanup.sh to avoid stupid
202 consistency with existing code. Run ``scripts/run-all-cleanup`` before
86 whitespace noise in your patches.
203 committing to ensure some basic code formatting consistency.
87
204
88 We support both Python 2.6.x and 2.7.x and nothing else. For now we don't care
205 We support both Python 2.6.x and 2.7.x and nothing else. For now we don't care
89 about Python 3 compatibility.
206 about Python 3 compatibility.
@@ -112,30 +229,66 b' page titles, button labels, headers, and'
112
229
113 .. _English title case: https://en.wikipedia.org/wiki/Capitalization#Title_case
230 .. _English title case: https://en.wikipedia.org/wiki/Capitalization#Title_case
114
231
115 Contributions will be accepted in most formats -- such as pull requests on
232 Template helpers (that is, everything in ``kallithea.lib.helpers``)
116 bitbucket, something hosted on your own Kallithea instance, or patches sent by
233 should only be referenced from templates. If you need to call a
117 email to the `kallithea-general`_ mailing list.
234 helper from the Python code, consider moving the function somewhere
235 else (e.g. to the model).
236
237 Notes on the SQLAlchemy session
238 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
239
240 Each HTTP request runs inside an independent SQLAlchemy session (as well
241 as in an independent database transaction). ``Session`` is the session manager
242 and factory. ``Session()`` will create a new session on-demand or return the
243 current session for the active thread. Many database operations are methods on
244 such session instances - only ``Session.remove()`` should be called directly on
245 the manager.
118
246
119 Make sure to test your changes both manually and with the automatic tests
247 Database model objects
120 before posting.
248 (almost) always belong to a particular SQLAlchemy session, which means
249 that SQLAlchemy will ensure that they're kept in sync with the database
250 (but also means that they cannot be shared across requests).
121
251
122 We care about quality and review and keeping a clean repository history. We
252 Objects can be added to the session using ``Session().add``, but this is
123 might give feedback that requests polishing contributions until they are
253 rarely needed:
124 "perfect". We might also rebase and collapse and make minor adjustments to your
254
125 changes when we apply them.
255 * When creating a database object by calling the constructor directly,
256 it must explicitly be added to the session.
257
258 * When creating an object using a factory function (like
259 ``create_repo``), the returned object has already (by convention)
260 been added to the session, and should not be added again.
126
261
127 We try to make sure we have consensus on the direction the project is taking.
262 * When getting an object from the session (via ``Session().query`` or
128 Everything non-sensitive should be discussed in public -- preferably on the
263 any of the utility functions that look up objects in the database),
129 mailing list. We aim at having all non-trivial changes reviewed by at least
264 it's already part of the session, and should not be added again.
130 one other core developer before pushing. Obvious non-controversial changes will
265 SQLAlchemy monitors attribute modifications automatically for all
131 be handled more casually.
266 objects it knows about and syncs them to the database.
267
268 SQLAlchemy also flushes changes to the database automatically; manually
269 calling ``Session().flush`` is usually only necessary when the Python
270 code needs the database to assign an "auto-increment" primary key ID to
271 a freshly created model object (before flushing, the ID attribute will
272 be ``None``).
273
274 TurboGears2 DebugBar
275 ^^^^^^^^^^^^^^^^^^^^
132
276
133 For now we just have one official branch ("default") and will keep it so stable
277 It is possible to enable the TurboGears2-provided DebugBar_, a toolbar overlayed
134 that it can be (and is) used in production. Experimental changes should live
278 over the Kallithea web interface, allowing you to see:
135 elsewhere (for example in a pull request) until they are ready.
279
280 * timing information of the current request, including profiling information
281 * request data, including GET data, POST data, cookies, headers and environment
282 variables
283 * a list of executed database queries, including timing and result values
136
284
137 .. _translations:
285 DebugBar is only activated when ``debug = true`` is set in the configuration
138 .. include:: ./../kallithea/i18n/how_to
286 file. This is important, because the DebugBar toolbar will be visible for all
287 users, and allow them to see information they should not be allowed to see. Like
288 is anyway the case for ``debug = true``, do not use this in production!
289
290 To enable DebugBar, install ``tgext.debugbar`` and ``kajiki`` (typically via
291 ``pip``) and restart Kallithea (in debug mode).
139
292
140
293
141 "Roadmap"
294 "Roadmap"
@@ -158,3 +311,6 b' Thank you for your contribution!'
158 .. _kallithea-general: http://lists.sfconservancy.org/mailman/listinfo/kallithea-general
311 .. _kallithea-general: http://lists.sfconservancy.org/mailman/listinfo/kallithea-general
159 .. _Hosted Weblate: https://hosted.weblate.org/projects/kallithea/kallithea/
312 .. _Hosted Weblate: https://hosted.weblate.org/projects/kallithea/kallithea/
160 .. _wiki: https://bitbucket.org/conservancy/kallithea/wiki/Home
313 .. _wiki: https://bitbucket.org/conservancy/kallithea/wiki/Home
314 .. _DebugBar: https://github.com/TurboGears/tgext.debugbar
315 .. _Quick Start: https://www.mercurial-scm.org/wiki/QuickStart
316 .. _Beginners Guide: https://www.mercurial-scm.org/wiki/BeginnersGuides
@@ -4,14 +4,23 b''
4 Kallithea Documentation
4 Kallithea Documentation
5 #######################
5 #######################
6
6
7 **Readme**
7 * :ref:`genindex`
8 * :ref:`search`
9
10
11 Readme
12 ******
8
13
9 .. toctree::
14 .. toctree::
10 :maxdepth: 1
15 :maxdepth: 1
11
16
12 readme
17 readme
13
18
14 **Installation**
19
20 Administrator guide
21 *******************
22
23 **Installation and upgrade**
15
24
16 .. toctree::
25 .. toctree::
17 :maxdepth: 1
26 :maxdepth: 1
@@ -21,52 +30,53 b' Kallithea Documentation'
21 installation_win
30 installation_win
22 installation_win_old
31 installation_win_old
23 installation_iis
32 installation_iis
33 installation_puppet
34 upgrade
35
36 **Setup and configuration**
37
38 .. toctree::
39 :maxdepth: 1
40
24 setup
41 setup
25 installation_puppet
42 administrator_guide/auth
43 administrator_guide/vcs_setup
44 usage/email
45 usage/customization
46
47 **Maintenance**
26
48
27 **Usage**
49 .. toctree::
50 :maxdepth: 1
51
52 usage/backup
53 usage/performance
54 usage/debugging
55 usage/troubleshooting
56
57
58 User guide
59 **********
28
60
29 .. toctree::
61 .. toctree::
30 :maxdepth: 1
62 :maxdepth: 1
31
63
32 usage/general
64 usage/general
33 usage/vcs_support
65 usage/vcs_notes
34 usage/locking
66 usage/locking
35 usage/statistics
67 usage/statistics
36
68 api/api
37 **Administrator's guide**
38
69
39 .. toctree::
40 :maxdepth: 1
41
70
42 usage/email
71 Developer guide
43 usage/performance
72 ***************
44 usage/backup
45 usage/debugging
46 usage/troubleshooting
47
48 **Development**
49
73
50 .. toctree::
74 .. toctree::
51 :maxdepth: 1
75 :maxdepth: 1
52
76
53 contributing
77 contributing
54 changelog
78 dev/translation
55
79 dev/dbmigrations
56 **API**
57
58 .. toctree::
59 :maxdepth: 1
60
61 api/api
62 api/models
63
64
65 Other topics
66 ------------
67
68 * :ref:`genindex`
69 * :ref:`search`
70
80
71
81
72 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
82 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
@@ -26,6 +26,22 b' The following describes three different '
26 have to remove its dependencies manually and make sure that they are not
26 have to remove its dependencies manually and make sure that they are not
27 needed by other packages.
27 needed by other packages.
28
28
29 Regardless of the installation method you may need to make sure you have
30 appropriate development packages installed, as installation of some of the
31 Kallithea dependencies requires a working C compiler and libffi library
32 headers. Depending on your configuration, you may also need to install
33 Git and development packages for the database of your choice.
34
35 For Debian and Ubuntu, the following command will ensure that a reasonable
36 set of dependencies is installed::
37
38 sudo apt-get install build-essential git python-pip python-virtualenv libffi-dev python-dev
39
40 For Fedora and RHEL-derivatives, the following command will ensure that a
41 reasonable set of dependencies is installed::
42
43 sudo yum install gcc git python-pip python-virtualenv libffi-devel python-devel
44
29 .. _installation-source:
45 .. _installation-source:
30
46
31
47
@@ -38,16 +54,13 b' repository, follow the instructions belo'
38 hg clone https://kallithea-scm.org/repos/kallithea -u stable
54 hg clone https://kallithea-scm.org/repos/kallithea -u stable
39 cd kallithea
55 cd kallithea
40 virtualenv ../kallithea-venv
56 virtualenv ../kallithea-venv
41 source ../kallithea-venv/bin/activate
57 . ../kallithea-venv/bin/activate
42 pip install --upgrade pip "setuptools<34"
58 pip install --upgrade pip setuptools
43 pip install -e .
59 pip install --upgrade -e .
44 python2 setup.py compile_catalog # for translation of the UI
60 python2 setup.py compile_catalog # for translation of the UI
45
61
46 You can now proceed to :ref:`setup`.
62 You can now proceed to :ref:`setup`.
47
63
48 To upgrade, simply update the repository with ``hg pull -u`` and restart the
49 server.
50
51 .. _installation-virtualenv:
64 .. _installation-virtualenv:
52
65
53
66
@@ -68,8 +81,8 b' An additional benefit of virtualenv_ is '
68 - Activate the virtualenv_ in your current shell session and make sure the
81 - Activate the virtualenv_ in your current shell session and make sure the
69 basic requirements are up-to-date by running::
82 basic requirements are up-to-date by running::
70
83
71 source /srv/kallithea/venv/bin/activate
84 . /srv/kallithea/venv/bin/activate
72 pip install --upgrade pip "setuptools<34"
85 pip install --upgrade pip setuptools
73
86
74 .. note:: You can't use UNIX ``sudo`` to source the ``virtualenv`` script; it
87 .. note:: You can't use UNIX ``sudo`` to source the ``virtualenv`` script; it
75 will "activate" a shell that terminates immediately. It is also perfectly
88 will "activate" a shell that terminates immediately. It is also perfectly
@@ -78,8 +91,8 b' An additional benefit of virtualenv_ is '
78 .. note:: Some dependencies are optional. If you need them, install them in
91 .. note:: Some dependencies are optional. If you need them, install them in
79 the virtualenv too::
92 the virtualenv too::
80
93
81 pip install psycopg2
94 pip install --upgrade psycopg2
82 pip install python-ldap
95 pip install --upgrade python-ldap
83
96
84 This might require installation of development packages using your
97 This might require installation of development packages using your
85 distribution's package manager.
98 distribution's package manager.
@@ -91,15 +104,15 b' An additional benefit of virtualenv_ is '
91
104
92 - Go into the created directory and run this command to install Kallithea::
105 - Go into the created directory and run this command to install Kallithea::
93
106
94 pip install kallithea
107 pip install --upgrade kallithea
95
108
96 Alternatively, download a .tar.gz from http://pypi.python.org/pypi/Kallithea,
109 Alternatively, download a .tar.gz from http://pypi.python.org/pypi/Kallithea,
97 extract it and run::
110 extract it and run::
98
111
99 pip install .
112 pip install --upgrade .
100
113
101 - This will install Kallithea together with pylons_ and all other required
114 - This will install Kallithea together with all other required
102 python libraries into the activated virtualenv.
115 Python libraries into the activated virtualenv.
103
116
104 You can now proceed to :ref:`setup`.
117 You can now proceed to :ref:`setup`.
105
118
@@ -123,90 +136,4 b' To install as a regular user in ``~/.loc'
123 You can now proceed to :ref:`setup`.
136 You can now proceed to :ref:`setup`.
124
137
125
138
126 Upgrading Kallithea from Python Package Index (PyPI)
127 ----------------------------------------------------
128
129 .. note::
130 It is strongly recommended that you **always** perform a database and
131 configuration backup before doing an upgrade.
132
133 These directions will use '{version}' to note that this is the version of
134 Kallithea that these files were used with. If backing up your Kallithea
135 instance from version 0.1 to 0.2, the ``my.ini`` file could be
136 backed up to ``my.ini.0-1``.
137
138 If using a SQLite database, stop the Kallithea process/daemon/service, and
139 then make a copy of the database file::
140
141 service kallithea stop
142 cp kallithea.db kallithea.db.{version}
143
144 Back up your configuration file::
145
146 cp my.ini my.ini.{version}
147
148 Ensure that you are using the Python virtual environment that you originally
149 installed Kallithea in by running::
150
151 pip freeze
152
153 This will list all packages installed in the current environment. If
154 Kallithea isn't listed, activate the correct virtual environment::
155
156 source /srv/kallithea/venv/bin/activate
157
158 Once you have verified the environment you can upgrade Kallithea with::
159
160 pip install --upgrade kallithea
161
162 Then run the following command from the installation directory::
163
164 paster make-config Kallithea my.ini
165
166 This will display any changes made by the new version of Kallithea to your
167 current configuration. It will try to perform an automerge. It is recommended
168 that you recheck the content after the automerge.
169
170 .. note::
171 Please always make sure your .ini files are up to date. Errors can
172 often be caused by missing parameters added in new versions.
173
174 It is also recommended that you rebuild the whoosh index after upgrading since
175 the new whoosh version could introduce some incompatible index changes. Please
176 read the changelog to see if there were any changes to whoosh.
177
178 The final step is to upgrade the database. To do this simply run::
179
180 paster upgrade-db my.ini
181
182 This will upgrade the schema and update some of the defaults in the database,
183 and will always recheck the settings of the application, if there are no new
184 options that need to be set.
185
186 .. note::
187 The DB schema upgrade library has some limitations and can sometimes fail if you try to
188 upgrade from older major releases. In such a case simply run upgrades sequentially, e.g.,
189 upgrading from 0.1.X to 0.3.X should be done like this: 0.1.X. > 0.2.X > 0.3.X
190 You can always specify what version of Kallithea you want to install for example in pip
191 `pip install Kallithea==0.2`
192
193 You may find it helpful to clear out your log file so that new errors are
194 readily apparent::
195
196 echo > kallithea.log
197
198 Once that is complete, you may now start your upgraded Kallithea Instance::
199
200 service kallithea start
201
202 Or::
203
204 paster serve /srv/kallithea/my.ini
205
206 .. note::
207 If you're using Celery, make sure you restart all instances of it after
208 upgrade.
209
210
211 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
139 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
212 .. _pylons: http://www.pylonsproject.org/
@@ -39,7 +39,7 b' will be served from the root of its own '
39 own virtual folder will be noted where appropriate.
39 own virtual folder will be noted where appropriate.
40
40
41 Application pool
41 Application pool
42 ................
42 ^^^^^^^^^^^^^^^^
43
43
44 Make sure that there is a unique application pool for the Kallithea application
44 Make sure that there is a unique application pool for the Kallithea application
45 with an identity that has read access to the Kallithea distribution.
45 with an identity that has read access to the Kallithea distribution.
@@ -55,11 +55,11 b' to run on the website and neither will K'
55 as long as the Kallithea requirements are met by the existing pool.
55 as long as the Kallithea requirements are met by the existing pool.
56
56
57 ISAPI handler
57 ISAPI handler
58 .............
58 ^^^^^^^^^^^^^
59
59
60 The ISAPI handler can be generated using::
60 The ISAPI handler can be generated using::
61
61
62 paster install-iis my.ini --virtualdir=/
62 kallithea-cli iis-install -c my.ini --virtualdir=/
63
63
64 This will generate a ``dispatch.py`` file in the current directory that contains
64 This will generate a ``dispatch.py`` file in the current directory that contains
65 the necessary components to finalize an installation into IIS. Once this file
65 the necessary components to finalize an installation into IIS. Once this file
@@ -74,12 +74,12 b' This accomplishes two things: generating'
74
74
75 The ISAPI handler is registered to all file extensions, so it will automatically
75 The ISAPI handler is registered to all file extensions, so it will automatically
76 be the one handling all requests to the specified virtual directory. When the website starts
76 be the one handling all requests to the specified virtual directory. When the website starts
77 the ISAPI handler, it will start a thread pool managed wrapper around the paster
77 the ISAPI handler, it will start a thread pool managed wrapper around the
78 middleware WSGI handler that Kallithea runs within and each HTTP request to the
78 middleware WSGI handler that Kallithea runs within and each HTTP request to the
79 site will be processed through this logic henceforth.
79 site will be processed through this logic henceforth.
80
80
81 Authentication with Kallithea using IIS authentication modules
81 Authentication with Kallithea using IIS authentication modules
82 ..............................................................
82 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
83
83
84 The recommended way to handle authentication with Kallithea using IIS is to let
84 The recommended way to handle authentication with Kallithea using IIS is to let
85 IIS handle all the authentication and just pass it to Kallithea.
85 IIS handle all the authentication and just pass it to Kallithea.
@@ -111,7 +111,7 b' Troubleshooting'
111 ---------------
111 ---------------
112
112
113 Typically, any issues in this setup will either be entirely in IIS or entirely
113 Typically, any issues in this setup will either be entirely in IIS or entirely
114 in Kallithea (or Kallithea's WSGI/paster middleware). Consequently, two
114 in Kallithea (or Kallithea's WSGI middleware). Consequently, two
115 different options for finding issues exist: IIS' failed request tracking which
115 different options for finding issues exist: IIS' failed request tracking which
116 is great at finding issues until they exist inside Kallithea, at which point the
116 is great at finding issues until they exist inside Kallithea, at which point the
117 ISAPI-WSGI wrapper above uses ``win32traceutil``, which is part of ``pywin32``.
117 ISAPI-WSGI wrapper above uses ``win32traceutil``, which is part of ``pywin32``.
@@ -90,7 +90,7 b' customizing the setup process. You have '
90 parameter in the :ref:`example above <simple_manifest>`, but there are more.
90 parameter in the :ref:`example above <simple_manifest>`, but there are more.
91 For example, you can specify the installation directory, the name of the user
91 For example, you can specify the installation directory, the name of the user
92 under which Kallithea gets installed, the initial admin password, etc.
92 under which Kallithea gets installed, the initial admin password, etc.
93 Notably, you can provide arbitrary modifications to Kallitheas configuration
93 Notably, you can provide arbitrary modifications to Kallithea's configuration
94 file by means of the ``config_hash`` parameter.
94 file by means of the ``config_hash`` parameter.
95
95
96 Parameters, which have not been set explicitly, will be set to default values,
96 Parameters, which have not been set explicitly, will be set to default values,
@@ -1,12 +1,12 b''
1 .. _installation_win:
1 .. _installation_win:
2
2
3 ================================================================
3 ====================================================
4 Installation and upgrade on Windows (7/Server 2008 R2 and newer)
4 Installation on Windows (7/Server 2008 R2 and newer)
5 ================================================================
5 ====================================================
6
6
7
7
8 First time install
8 First time install
9 ::::::::::::::::::
9 ------------------
10
10
11 Target OS: Windows 7 and newer or Windows Server 2008 R2 and newer
11 Target OS: Windows 7 and newer or Windows Server 2008 R2 and newer
12
12
@@ -15,7 +15,7 b' Tested on Windows 8.1, Windows Server 20'
15 To install on an older version of Windows, see `<installation_win_old.html>`_
15 To install on an older version of Windows, see `<installation_win_old.html>`_
16
16
17 Step 1 -- Install Python
17 Step 1 -- Install Python
18 ------------------------
18 ^^^^^^^^^^^^^^^^^^^^^^^^
19
19
20 Install Python 2.x.y (x = 6 or 7). Latest version is recommended. If you need another version, they can run side by side.
20 Install Python 2.x.y (x = 6 or 7). Latest version is recommended. If you need another version, they can run side by side.
21
21
@@ -31,7 +31,7 b' Remember the specific major and minor ve'
31 be needed in the next step. In this case, it is "2.7".
31 be needed in the next step. In this case, it is "2.7".
32
32
33 Step 2 -- Python BIN
33 Step 2 -- Python BIN
34 --------------------
34 ^^^^^^^^^^^^^^^^^^^^
35
35
36 Add Python BIN folder to the path. This can be done manually (editing
36 Add Python BIN folder to the path. This can be done manually (editing
37 "PATH" environment variable) or by using Windows Support Tools that
37 "PATH" environment variable) or by using Windows Support Tools that
@@ -45,7 +45,7 b' Please substitute [your-python-path] wit'
45 path. Typically this is ``C:\\Python27``.
45 path. Typically this is ``C:\\Python27``.
46
46
47 Step 3 -- Install pywin32 extensions
47 Step 3 -- Install pywin32 extensions
48 ------------------------------------
48 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
49
49
50 Download pywin32 from:
50 Download pywin32 from:
51 http://sourceforge.net/projects/pywin32/files/
51 http://sourceforge.net/projects/pywin32/files/
@@ -61,7 +61,7 b' http://sourceforge.net/projects/pywin32/'
61 (Win32)
61 (Win32)
62
62
63 Step 4 -- Install pip
63 Step 4 -- Install pip
64 ---------------------
64 ^^^^^^^^^^^^^^^^^^^^^
65
65
66 pip is a package management system for Python. You will need it to install Kallithea and its dependencies.
66 pip is a package management system for Python. You will need it to install Kallithea and its dependencies.
67
67
@@ -85,7 +85,7 b' open a CMD and type::'
85 SETX PATH "%PATH%;[your-python-path]\Scripts" /M
85 SETX PATH "%PATH%;[your-python-path]\Scripts" /M
86
86
87 Step 5 -- Kallithea folder structure
87 Step 5 -- Kallithea folder structure
88 ------------------------------------
88 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
89
89
90 Create a Kallithea folder structure.
90 Create a Kallithea folder structure.
91
91
@@ -102,7 +102,7 b' Create the following folder structure::'
102 C:\Kallithea\Repos
102 C:\Kallithea\Repos
103
103
104 Step 6 -- Install virtualenv
104 Step 6 -- Install virtualenv
105 ----------------------------
105 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
106
106
107 .. note::
107 .. note::
108 A python virtual environment will allow for isolation between the Python packages of your system and those used for Kallithea.
108 A python virtual environment will allow for isolation between the Python packages of your system and those used for Kallithea.
@@ -119,7 +119,7 b' To create a virtual environment, run::'
119 virtualenv C:\Kallithea\Env
119 virtualenv C:\Kallithea\Env
120
120
121 Step 7 -- Install Kallithea
121 Step 7 -- Install Kallithea
122 ---------------------------
122 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
123
123
124 In order to install Kallithea, you need to be able to run "pip install kallithea". It will use pip to install the Kallithea Python package and its dependencies.
124 In order to install Kallithea, you need to be able to run "pip install kallithea". It will use pip to install the Kallithea Python package and its dependencies.
125 Some Python packages use managed code and need to be compiled.
125 Some Python packages use managed code and need to be compiled.
@@ -134,7 +134,7 b' In a command prompt type (adapting paths'
134
134
135 cd C:\Kallithea\Env\Scripts
135 cd C:\Kallithea\Env\Scripts
136 activate
136 activate
137 pip install --upgrade pip "setuptools<34"
137 pip install --upgrade pip setuptools
138
138
139 The prompt will change into "(Env) C:\\Kallithea\\Env\\Scripts" or similar
139 The prompt will change into "(Env) C:\\Kallithea\\Env\\Scripts" or similar
140 (depending of your folder structure). Then type::
140 (depending of your folder structure). Then type::
@@ -145,17 +145,19 b' The prompt will change into "(Env) C:\\\\K'
145 complete. Some warnings will appear. Don't worry, they are
145 complete. Some warnings will appear. Don't worry, they are
146 normal.
146 normal.
147
147
148 Step 8 -- Install git (optional)
148 Step 8 -- Install Git (optional)
149 --------------------------------
149 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
150
151 Mercurial being a python package, was installed automatically when doing ``pip install kallithea``.
150
152
151 Mercurial being a python package, it was installed automatically when doing "pip install kallithea".
153 You need to install Git manually if you want Kallithea to be able to host Git repositories.
152
153 You need to install git manually if you want Kallithea to be able to host git repositories.
154
155 See http://git-scm.com/book/en/v2/Getting-Started-Installing-Git#Installing-on-Windows for instructions.
154 See http://git-scm.com/book/en/v2/Getting-Started-Installing-Git#Installing-on-Windows for instructions.
155 The location of the Git binaries (like ``c:\path\to\git\bin``) must be
156 added to the ``PATH`` environment variable so ``git.exe`` and other tools like
157 ``gzip.exe`` are available.
156
158
157 Step 9 -- Configuring Kallithea
159 Step 9 -- Configuring Kallithea
158 -------------------------------
160 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
159
161
160 Steps taken from `<setup.html>`_
162 Steps taken from `<setup.html>`_
161
163
@@ -164,9 +166,9 b' it, reopen it following the same command'
164 one). When ready, type::
166 one). When ready, type::
165
167
166 cd C:\Kallithea\Bin
168 cd C:\Kallithea\Bin
167 paster make-config Kallithea production.ini
169 kallithea-cli config-create my.ini
168
170
169 Then you must edit production.ini to fit your needs (IP address, IP
171 Then you must edit my.ini to fit your needs (IP address, IP
170 port, mail settings, database, etc.). `NotePad++`__ or a similar text
172 port, mail settings, database, etc.). `NotePad++`__ or a similar text
171 editor is recommended to properly handle the newline character
173 editor is recommended to properly handle the newline character
172 differences between Unix and Windows.
174 differences between Unix and Windows.
@@ -175,11 +177,11 b' differences between Unix and Windows.'
175
177
176 For the sake of simplicity, run it with the default settings. After your edits (if any) in the previous command prompt, type::
178 For the sake of simplicity, run it with the default settings. After your edits (if any) in the previous command prompt, type::
177
179
178 paster setup-db production.ini
180 kallithea-cli db-create -c my.ini
179
181
180 .. warning:: This time a *new* database will be installed. You must
182 .. warning:: This time a *new* database will be installed. You must
181 follow a different step to later *upgrade* to a newer
183 follow a different process to later :ref:`upgrade <upgrade>`
182 Kallithea version)
184 to a newer Kallithea version.
183
185
184 The script will ask you for confirmation about creating a new database, answer yes (y)
186 The script will ask you for confirmation about creating a new database, answer yes (y)
185
187
@@ -191,14 +193,14 b' The script will ask you for admin mail, '
191
193
192 If you make a mistake and the script doesn't end, don't worry: start it again.
194 If you make a mistake and the script doesn't end, don't worry: start it again.
193
195
194 If you decided not to install git, you will get errors about it that you can ignore.
196 If you decided not to install Git, you will get errors about it that you can ignore.
195
197
196 Step 10 -- Running Kallithea
198 Step 10 -- Running Kallithea
197 ----------------------------
199 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
198
200
199 In the previous command prompt, being in the C:\\Kallithea\\Bin folder, type::
201 In the previous command prompt, being in the C:\\Kallithea\\Bin folder, type::
200
202
201 paster serve production.ini
203 gearbox serve -c my.ini
202
204
203 Open your web server, and go to http://127.0.0.1:5000
205 Open your web server, and go to http://127.0.0.1:5000
204
206
@@ -219,27 +221,3 b' What this guide does not cover:'
219 - Using Apache. You can investigate here:
221 - Using Apache. You can investigate here:
220
222
221 - https://groups.google.com/group/rhodecode/msg/c433074e813ffdc4
223 - https://groups.google.com/group/rhodecode/msg/c433074e813ffdc4
222
223
224 Upgrading
225 :::::::::
226
227 Stop running Kallithea
228 Open a CommandPrompt like in Step 7 (cd to C:\Kallithea\Env\Scripts and activate) and type::
229
230 pip install kallithea --upgrade
231 cd \Kallithea\Bin
232
233 Backup your production.ini file now.
234
235 Then run::
236
237 paster make-config Kallithea production.ini
238
239 Look for changes and update your production.ini accordingly.
240
241 Next, update the database::
242
243 paster upgrade-db production.ini
244
245 More details can be found in `<upgrade.html>`_.
@@ -1,12 +1,12 b''
1 .. _installation_win_old:
1 .. _installation_win_old:
2
2
3 ======================================================================
3 ==========================================================
4 Installation and upgrade on Windows (XP/Vista/Server 2003/Server 2008)
4 Installation on Windows (XP/Vista/Server 2003/Server 2008)
5 ======================================================================
5 ==========================================================
6
6
7
7
8 First-time install
8 First-time install
9 ::::::::::::::::::
9 ------------------
10
10
11 Target OS: Windows XP SP3 32-bit English (Clean installation)
11 Target OS: Windows XP SP3 32-bit English (Clean installation)
12 + All Windows Updates until 24-may-2012
12 + All Windows Updates until 24-may-2012
@@ -24,7 +24,7 b' Target OS: Windows XP SP3 32-bit English'
24 - http://bugs.python.org/issue7511
24 - http://bugs.python.org/issue7511
25
25
26 Step 1 -- Install Visual Studio 2008 Express
26 Step 1 -- Install Visual Studio 2008 Express
27 --------------------------------------------
27 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28
28
29 Optional: You can also install MinGW, but VS2008 installation is easier.
29 Optional: You can also install MinGW, but VS2008 installation is easier.
30
30
@@ -58,7 +58,7 b' choose "Visual C++ 2008 Express" when in'
58 Copy C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\bin\vcvars64.bat to C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\bin\amd64\vcvarsamd64.bat
58 Copy C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\bin\vcvars64.bat to C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\bin\amd64\vcvarsamd64.bat
59
59
60 Step 2 -- Install Python
60 Step 2 -- Install Python
61 ------------------------
61 ^^^^^^^^^^^^^^^^^^^^^^^^
62
62
63 Install Python 2.x.y (x = 6 or 7) x86 version (32-bit). DO NOT USE A 3.x version.
63 Install Python 2.x.y (x = 6 or 7) x86 version (32-bit). DO NOT USE A 3.x version.
64 Download Python 2.x.y from:
64 Download Python 2.x.y from:
@@ -74,7 +74,7 b' be needed in the next step. In this case'
74 64-bit: Just download and install the 64-bit version of python.
74 64-bit: Just download and install the 64-bit version of python.
75
75
76 Step 3 -- Install Win32py extensions
76 Step 3 -- Install Win32py extensions
77 ------------------------------------
77 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
78
78
79 Download pywin32 from:
79 Download pywin32 from:
80 http://sourceforge.net/projects/pywin32/files/
80 http://sourceforge.net/projects/pywin32/files/
@@ -93,7 +93,7 b' http://sourceforge.net/projects/pywin32/'
93 http://sourceforge.net/projects/pywin32/files/pywin32/Build%20218/pywin32-218.win-amd64-py2.7.exe/download
93 http://sourceforge.net/projects/pywin32/files/pywin32/Build%20218/pywin32-218.win-amd64-py2.7.exe/download
94
94
95 Step 4 -- Python BIN
95 Step 4 -- Python BIN
96 --------------------
96 ^^^^^^^^^^^^^^^^^^^^
97
97
98 Add Python BIN folder to the path
98 Add Python BIN folder to the path
99
99
@@ -120,7 +120,7 b' that came preinstalled in Vista/7 and ca'
120 Typically: C:\\Python27
120 Typically: C:\\Python27
121
121
122 Step 5 -- Kallithea folder structure
122 Step 5 -- Kallithea folder structure
123 ------------------------------------
123 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
124
124
125 Create a Kallithea folder structure
125 Create a Kallithea folder structure
126
126
@@ -137,7 +137,7 b' Create the following folder structure::'
137 C:\Kallithea\Repos
137 C:\Kallithea\Repos
138
138
139 Step 6 -- Install virtualenv
139 Step 6 -- Install virtualenv
140 ----------------------------
140 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
141
141
142 Install Virtual Env for Python
142 Install Virtual Env for Python
143
143
@@ -157,7 +157,7 b' where you downloaded "virtualenv.py", an'
157 to include it)
157 to include it)
158
158
159 Step 7 -- Install Kallithea
159 Step 7 -- Install Kallithea
160 ---------------------------
160 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
161
161
162 Finally, install Kallithea
162 Finally, install Kallithea
163
163
@@ -183,7 +183,7 b' In that CMD (loaded with VS2008 PATHs) t'
183
183
184 cd C:\Kallithea\Env\Scripts (or similar)
184 cd C:\Kallithea\Env\Scripts (or similar)
185 activate
185 activate
186 pip install --upgrade pip "setuptools<34"
186 pip install --upgrade pip setuptools
187
187
188 The prompt will change into "(Env) C:\\Kallithea\\Env\\Scripts" or similar
188 The prompt will change into "(Env) C:\\Kallithea\\Env\\Scripts" or similar
189 (depending of your folder structure). Then type::
189 (depending of your folder structure). Then type::
@@ -195,7 +195,7 b' The prompt will change into "(Env) C:\\\\K'
195 Some warnings will appear, don't worry as they are normal.
195 Some warnings will appear, don't worry as they are normal.
196
196
197 Step 8 -- Configuring Kallithea
197 Step 8 -- Configuring Kallithea
198 -------------------------------
198 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
199
199
200 steps taken from http://packages.python.org/Kallithea/setup.html
200 steps taken from http://packages.python.org/Kallithea/setup.html
201
201
@@ -204,9 +204,9 b' if you closed it reopen it following the'
204 "activate" one). When ready, just type::
204 "activate" one). When ready, just type::
205
205
206 cd C:\Kallithea\Bin
206 cd C:\Kallithea\Bin
207 paster make-config Kallithea production.ini
207 kallithea-cli config-create my.ini
208
208
209 Then, you must edit production.ini to fit your needs (network address and
209 Then, you must edit my.ini to fit your needs (network address and
210 port, mail settings, database, whatever). I recommend using NotePad++
210 port, mail settings, database, whatever). I recommend using NotePad++
211 (free) or similar text editor, as it handles well the EndOfLine
211 (free) or similar text editor, as it handles well the EndOfLine
212 character differences between Unix and Windows
212 character differences between Unix and Windows
@@ -215,10 +215,11 b' character differences between Unix and W'
215 For the sake of simplicity lets run it with the default settings. After
215 For the sake of simplicity lets run it with the default settings. After
216 your edits (if any), in the previous Command Prompt, type::
216 your edits (if any), in the previous Command Prompt, type::
217
217
218 paster setup-db production.ini
218 kallithea-cli db-create -c my.ini
219
219
220 (this time a NEW database will be installed, you must follow a different
220 .. warning:: This time a *new* database will be installed. You must
221 step to later UPGRADE to a newer Kallithea version)
221 follow a different process to later :ref:`upgrade <upgrade>`
222 to a newer Kallithea version.
222
223
223 The script will ask you for confirmation about creating a NEW database,
224 The script will ask you for confirmation about creating a NEW database,
224 answer yes (y)
225 answer yes (y)
@@ -233,12 +234,12 b' If you make some mistake and the script '
233 it again.
234 it again.
234
235
235 Step 9 -- Running Kallithea
236 Step 9 -- Running Kallithea
236 ---------------------------
237 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
237
238
238 In the previous command prompt, being in the C:\\Kallithea\\Bin folder,
239 In the previous command prompt, being in the C:\\Kallithea\\Bin folder,
239 just type::
240 just type::
240
241
241 paster serve production.ini
242 gearbox serve -c my.ini
242
243
243 Open yout web server, and go to http://127.0.0.1:5000
244 Open yout web server, and go to http://127.0.0.1:5000
244
245
@@ -260,23 +261,3 b' What this Guide does not cover:'
260 - Using Apache. You can investigate here:
261 - Using Apache. You can investigate here:
261
262
262 - https://groups.google.com/group/rhodecode/msg/c433074e813ffdc4
263 - https://groups.google.com/group/rhodecode/msg/c433074e813ffdc4
263
264
265 Upgrading
266 :::::::::
267
268 Stop running Kallithea
269 Open a CommandPrompt like in Step7 (VS2008 path + activate) and type::
270
271 easy_install -U kallithea
272 cd \Kallithea\Bin
273
274 { backup your production.ini file now} ::
275
276 paster make-config Kallithea production.ini
277
278 (check changes and update your production.ini accordingly) ::
279
280 paster upgrade-db production.ini (update database)
281
282 Full steps in http://packages.python.org/Kallithea/upgrade.html
@@ -3,153 +3,153 b''
3 REM Command file for Sphinx documentation
3 REM Command file for Sphinx documentation
4
4
5 if "%SPHINXBUILD%" == "" (
5 if "%SPHINXBUILD%" == "" (
6 set SPHINXBUILD=sphinx-build
6 set SPHINXBUILD=sphinx-build
7 )
7 )
8 set BUILDDIR=_build
8 set BUILDDIR=_build
9 set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
9 set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
10 if NOT "%PAPER%" == "" (
10 if NOT "%PAPER%" == "" (
11 set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
11 set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
12 )
12 )
13
13
14 if "%1" == "" goto help
14 if "%1" == "" goto help
15
15
16 if "%1" == "help" (
16 if "%1" == "help" (
17 :help
17 :help
18 echo.Please use `make ^<target^>` where ^<target^> is one of
18 echo.Please use `make ^<target^>` where ^<target^> is one of
19 echo. html to make standalone HTML files
19 echo. html to make standalone HTML files
20 echo. dirhtml to make HTML files named index.html in directories
20 echo. dirhtml to make HTML files named index.html in directories
21 echo. singlehtml to make a single large HTML file
21 echo. singlehtml to make a single large HTML file
22 echo. pickle to make pickle files
22 echo. pickle to make pickle files
23 echo. json to make JSON files
23 echo. json to make JSON files
24 echo. htmlhelp to make HTML files and a HTML help project
24 echo. htmlhelp to make HTML files and a HTML help project
25 echo. qthelp to make HTML files and a qthelp project
25 echo. qthelp to make HTML files and a qthelp project
26 echo. devhelp to make HTML files and a Devhelp project
26 echo. devhelp to make HTML files and a Devhelp project
27 echo. epub to make an epub
27 echo. epub to make an epub
28 echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
28 echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
29 echo. text to make text files
29 echo. text to make text files
30 echo. man to make manual pages
30 echo. man to make manual pages
31 echo. changes to make an overview over all changed/added/deprecated items
31 echo. changes to make an overview over all changed/added/deprecated items
32 echo. linkcheck to check all external links for integrity
32 echo. linkcheck to check all external links for integrity
33 echo. doctest to run all doctests embedded in the documentation if enabled
33 echo. doctest to run all doctests embedded in the documentation if enabled
34 goto end
34 goto end
35 )
35 )
36
36
37 if "%1" == "clean" (
37 if "%1" == "clean" (
38 for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
38 for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
39 del /q /s %BUILDDIR%\*
39 del /q /s %BUILDDIR%\*
40 goto end
40 goto end
41 )
41 )
42
42
43 if "%1" == "html" (
43 if "%1" == "html" (
44 %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
44 %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
45 echo.
45 echo.
46 echo.Build finished. The HTML pages are in %BUILDDIR%/html.
46 echo.Build finished. The HTML pages are in %BUILDDIR%/html.
47 goto end
47 goto end
48 )
48 )
49
49
50 if "%1" == "dirhtml" (
50 if "%1" == "dirhtml" (
51 %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
51 %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
52 echo.
52 echo.
53 echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
53 echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
54 goto end
54 goto end
55 )
55 )
56
56
57 if "%1" == "singlehtml" (
57 if "%1" == "singlehtml" (
58 %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
58 %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
59 echo.
59 echo.
60 echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
60 echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
61 goto end
61 goto end
62 )
62 )
63
63
64 if "%1" == "pickle" (
64 if "%1" == "pickle" (
65 %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
65 %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
66 echo.
66 echo.
67 echo.Build finished; now you can process the pickle files.
67 echo.Build finished; now you can process the pickle files.
68 goto end
68 goto end
69 )
69 )
70
70
71 if "%1" == "json" (
71 if "%1" == "json" (
72 %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
72 %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
73 echo.
73 echo.
74 echo.Build finished; now you can process the JSON files.
74 echo.Build finished; now you can process the JSON files.
75 goto end
75 goto end
76 )
76 )
77
77
78 if "%1" == "htmlhelp" (
78 if "%1" == "htmlhelp" (
79 %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
79 %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
80 echo.
80 echo.
81 echo.Build finished; now you can run HTML Help Workshop with the ^
81 echo.Build finished; now you can run HTML Help Workshop with the ^
82 .hhp project file in %BUILDDIR%/htmlhelp.
82 .hhp project file in %BUILDDIR%/htmlhelp.
83 goto end
83 goto end
84 )
84 )
85
85
86 if "%1" == "qthelp" (
86 if "%1" == "qthelp" (
87 %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
87 %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
88 echo.
88 echo.
89 echo.Build finished; now you can run "qcollectiongenerator" with the ^
89 echo.Build finished; now you can run "qcollectiongenerator" with the ^
90 .qhcp project file in %BUILDDIR%/qthelp, like this:
90 .qhcp project file in %BUILDDIR%/qthelp, like this:
91 echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Kallithea.qhcp
91 echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Kallithea.qhcp
92 echo.To view the help file:
92 echo.To view the help file:
93 echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Kallithea.ghc
93 echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Kallithea.ghc
94 goto end
94 goto end
95 )
95 )
96
96
97 if "%1" == "devhelp" (
97 if "%1" == "devhelp" (
98 %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
98 %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
99 echo.
99 echo.
100 echo.Build finished.
100 echo.Build finished.
101 goto end
101 goto end
102 )
102 )
103
103
104 if "%1" == "epub" (
104 if "%1" == "epub" (
105 %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
105 %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
106 echo.
106 echo.
107 echo.Build finished. The epub file is in %BUILDDIR%/epub.
107 echo.Build finished. The epub file is in %BUILDDIR%/epub.
108 goto end
108 goto end
109 )
109 )
110
110
111 if "%1" == "latex" (
111 if "%1" == "latex" (
112 %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
112 %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
113 echo.
113 echo.
114 echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
114 echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
115 goto end
115 goto end
116 )
116 )
117
117
118 if "%1" == "text" (
118 if "%1" == "text" (
119 %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
119 %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
120 echo.
120 echo.
121 echo.Build finished. The text files are in %BUILDDIR%/text.
121 echo.Build finished. The text files are in %BUILDDIR%/text.
122 goto end
122 goto end
123 )
123 )
124
124
125 if "%1" == "man" (
125 if "%1" == "man" (
126 %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
126 %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
127 echo.
127 echo.
128 echo.Build finished. The manual pages are in %BUILDDIR%/man.
128 echo.Build finished. The manual pages are in %BUILDDIR%/man.
129 goto end
129 goto end
130 )
130 )
131
131
132 if "%1" == "changes" (
132 if "%1" == "changes" (
133 %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
133 %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
134 echo.
134 echo.
135 echo.The overview file is in %BUILDDIR%/changes.
135 echo.The overview file is in %BUILDDIR%/changes.
136 goto end
136 goto end
137 )
137 )
138
138
139 if "%1" == "linkcheck" (
139 if "%1" == "linkcheck" (
140 %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
140 %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
141 echo.
141 echo.
142 echo.Link check complete; look for any errors in the above output ^
142 echo.Link check complete; look for any errors in the above output ^
143 or in %BUILDDIR%/linkcheck/output.txt.
143 or in %BUILDDIR%/linkcheck/output.txt.
144 goto end
144 goto end
145 )
145 )
146
146
147 if "%1" == "doctest" (
147 if "%1" == "doctest" (
148 %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
148 %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
149 echo.
149 echo.
150 echo.Testing of doctests in the sources finished, look at the ^
150 echo.Testing of doctests in the sources finished, look at the ^
151 results in %BUILDDIR%/doctest/output.txt.
151 results in %BUILDDIR%/doctest/output.txt.
152 goto end
152 goto end
153 )
153 )
154
154
155 :end
155 :end
@@ -69,6 +69,11 b' installed.'
69 (``pip install kallithea`` from a source tree will do pretty much the same
69 (``pip install kallithea`` from a source tree will do pretty much the same
70 but build the Kallithea package itself locally instead of downloading it.)
70 but build the Kallithea package itself locally instead of downloading it.)
71
71
72 .. note::
73 Kallithea includes front-end code that needs to be processed first.
74 The tool npm_ is used to download external dependencies and orchestrate the
75 processing. The ``npm`` binary must thus be available.
76
72
77
73 Web server
78 Web server
74 ----------
79 ----------
@@ -84,17 +89,17 b' to use web server authentication.'
84
89
85 There are several web server options:
90 There are several web server options:
86
91
87 - Kallithea uses the Paste_ tool as command line interface. Paste provides
92 - Kallithea uses the Gearbox_ tool as command line interface. Gearbox provides
88 ``paster serve`` as a convenient way to launch a Python WSGI / web server
93 ``gearbox serve`` as a convenient way to launch a Python WSGI / web server
89 from the command line. That is perfect for development and evaluation.
94 from the command line. That is perfect for development and evaluation.
90 Actual use in production might have different requirements and need extra
95 Actual use in production might have different requirements and need extra
91 work to make it manageable as a scalable system service.
96 work to make it manageable as a scalable system service.
92
97
93 Paste comes with its own built-in web server but Kallithea defaults to use
98 Gearbox comes with its own built-in web server but Kallithea defaults to use
94 Waitress_. Gunicorn_ is also an option. These web servers have different
99 Waitress_. Gunicorn_ is also an option. These web servers have different
95 limited feature sets.
100 limited feature sets.
96
101
97 The web server used by ``paster`` is configured in the ``.ini`` file passed
102 The web server used by ``gearbox`` is configured in the ``.ini`` file passed
98 to it. The entry point for the WSGI application is configured
103 to it. The entry point for the WSGI application is configured
99 in ``setup.py`` as ``kallithea.config.middleware:make_app``.
104 in ``setup.py`` as ``kallithea.config.middleware:make_app``.
100
105
@@ -113,7 +118,7 b' There are several web server options:'
113 encryption or special authentication or for other security reasons, to
118 encryption or special authentication or for other security reasons, to
114 provide caching of static files, or to provide load balancing or fail-over.
119 provide caching of static files, or to provide load balancing or fail-over.
115 Nginx_, Varnish_ and HAProxy_ are often used for this purpose, often in front
120 Nginx_, Varnish_ and HAProxy_ are often used for this purpose, often in front
116 of a ``paster`` server that somehow is wrapped as a service.
121 of a ``gearbox serve`` that somehow is wrapped as a service.
117
122
118 The best option depends on what you are familiar with and the requirements for
123 The best option depends on what you are familiar with and the requirements for
119 performance and stability. Also, keep in mind that Kallithea mainly is serving
124 performance and stability. Also, keep in mind that Kallithea mainly is serving
@@ -126,7 +131,7 b' continuous hammering from the internet.'
126 .. _Gunicorn: http://gunicorn.org/
131 .. _Gunicorn: http://gunicorn.org/
127 .. _Waitress: http://waitress.readthedocs.org/en/latest/
132 .. _Waitress: http://waitress.readthedocs.org/en/latest/
128 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
133 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
129 .. _Paste: http://pythonpaste.org/
134 .. _Gearbox: http://turbogears.readthedocs.io/en/latest/turbogears/gearbox.html
130 .. _PyPI: https://pypi.python.org/pypi
135 .. _PyPI: https://pypi.python.org/pypi
131 .. _Apache httpd: http://httpd.apache.org/
136 .. _Apache httpd: http://httpd.apache.org/
132 .. _mod_wsgi: https://code.google.com/p/modwsgi/
137 .. _mod_wsgi: https://code.google.com/p/modwsgi/
@@ -136,6 +141,6 b' continuous hammering from the internet.'
136 .. _iis: http://en.wikipedia.org/wiki/Internet_Information_Services
141 .. _iis: http://en.wikipedia.org/wiki/Internet_Information_Services
137 .. _pip: http://en.wikipedia.org/wiki/Pip_%28package_manager%29
142 .. _pip: http://en.wikipedia.org/wiki/Pip_%28package_manager%29
138 .. _WSGI: http://en.wikipedia.org/wiki/Web_Server_Gateway_Interface
143 .. _WSGI: http://en.wikipedia.org/wiki/Web_Server_Gateway_Interface
139 .. _pylons: http://www.pylonsproject.org/
140 .. _HAProxy: http://www.haproxy.org/
144 .. _HAProxy: http://www.haproxy.org/
141 .. _Varnish: https://www.varnish-cache.org/
145 .. _Varnish: https://www.varnish-cache.org/
146 .. _npm: https://www.npmjs.com/
@@ -11,12 +11,14 b' Setting up Kallithea'
11 First, you will need to create a Kallithea configuration file. Run the
11 First, you will need to create a Kallithea configuration file. Run the
12 following command to do so::
12 following command to do so::
13
13
14 paster make-config Kallithea my.ini
14 kallithea-cli config-create my.ini
15
15
16 This will create the file ``my.ini`` in the current directory. This
16 This will create the file ``my.ini`` in the current directory. This
17 configuration file contains the various settings for Kallithea, e.g.
17 configuration file contains the various settings for Kallithea, e.g.
18 proxy port, email settings, usage of static files, cache, Celery
18 proxy port, email settings, usage of static files, cache, Celery
19 settings, and logging.
19 settings, and logging. Extra settings can be specified like::
20
21 kallithea-cli config-create my.ini host=8.8.8.8 "[handler_console]" formatter=color_formatter
20
22
21 Next, you need to create the databases used by Kallithea. It is recommended to
23 Next, you need to create the databases used by Kallithea. It is recommended to
22 use PostgreSQL or SQLite (default). If you choose a database other than the
24 use PostgreSQL or SQLite (default). If you choose a database other than the
@@ -25,20 +27,20 b' configuration file to use this other dat'
25 PostgreSQL, SQLite and MySQL databases. Create the database by running
27 PostgreSQL, SQLite and MySQL databases. Create the database by running
26 the following command::
28 the following command::
27
29
28 paster setup-db my.ini
30 kallithea-cli db-create -c my.ini
29
31
30 This will prompt you for a "root" path. This "root" path is the location where
32 This will prompt you for a "root" path. This "root" path is the location where
31 Kallithea will store all of its repositories on the current machine. After
33 Kallithea will store all of its repositories on the current machine. After
32 entering this "root" path ``setup-db`` will also prompt you for a username
34 entering this "root" path ``db-create`` will also prompt you for a username
33 and password for the initial admin account which ``setup-db`` sets
35 and password for the initial admin account which ``db-create`` sets
34 up for you.
36 up for you.
35
37
36 The ``setup-db`` values can also be given on the command line.
38 The ``db-create`` values can also be given on the command line.
37 Example::
39 Example::
38
40
39 paster setup-db my.ini --user=nn --password=secret --email=nn@example.com --repos=/srv/repos
41 kallithea-cli db-create -c my.ini --user=nn --password=secret --email=nn@example.com --repos=/srv/repos
40
42
41 The ``setup-db`` command will create all needed tables and an
43 The ``db-create`` command will create all needed tables and an
42 admin account. When choosing a root path you can either use a new
44 admin account. When choosing a root path you can either use a new
43 empty location, or a location which already contains existing
45 empty location, or a location which already contains existing
44 repositories. If you choose a location which contains existing
46 repositories. If you choose a location which contains existing
@@ -52,14 +54,18 b' path to the root).'
52 but when trying to do a push it will fail with permission
54 but when trying to do a push it will fail with permission
53 denied errors unless it has write access.
55 denied errors unless it has write access.
54
56
57 Finally, prepare the front-end by running::
58
59 kallithea-cli front-end-build
60
55 You are now ready to use Kallithea. To run it simply execute::
61 You are now ready to use Kallithea. To run it simply execute::
56
62
57 paster serve my.ini
63 gearbox serve -c my.ini
58
64
59 - This command runs the Kallithea server. The web app should be available at
65 - This command runs the Kallithea server. The web app should be available at
60 http://127.0.0.1:5000. The IP address and port is configurable via the
66 http://127.0.0.1:5000. The IP address and port is configurable via the
61 configuration file created in the previous step.
67 configuration file created in the previous step.
62 - Log in to Kallithea using the admin account created when running ``setup-db``.
68 - Log in to Kallithea using the admin account created when running ``db-create``.
63 - The default permissions on each repository is read, and the owner is admin.
69 - The default permissions on each repository is read, and the owner is admin.
64 Remember to update these if needed.
70 Remember to update these if needed.
65 - In the admin panel you can toggle LDAP, anonymous, and permissions
71 - In the admin panel you can toggle LDAP, anonymous, and permissions
@@ -67,22 +73,20 b' You are now ready to use Kallithea. To r'
67 repositories.
73 repositories.
68
74
69
75
70 Extensions
76 Internationalization (i18n support)
71 ----------
77 -----------------------------------
72
73 Optionally one can create an ``rcextensions`` package that extends Kallithea
74 functionality.
75 To generate a skeleton extensions package, run::
76
78
77 paster make-rcext my.ini
79 The Kallithea web interface is automatically displayed in the user's preferred
80 language, as indicated by the browser. Thus, different users may see the
81 application in different languages. If the requested language is not available
82 (because the translation file for that language does not yet exist or is
83 incomplete), the language specified in setting ``i18n.lang`` in the Kallithea
84 configuration file is used as fallback. If no fallback language is explicitly
85 specified, English is used.
78
86
79 This will create an ``rcextensions`` package next to the specified ``ini`` file.
87 If you want to disable automatic language detection and instead configure a
80 With ``rcextensions`` it's possible to add additional mapping for whoosh,
88 fixed language regardless of user preference, set ``i18n.enabled = false`` and
81 stats and add additional code into the push/pull/create/delete repo hooks,
89 set ``i18n.lang`` to the desired language (or leave empty for English).
82 for example for sending signals to build-bots such as Jenkins.
83
84 See the ``__init__.py`` file inside the generated ``rcextensions`` package
85 for more details.
86
90
87
91
88 Using Kallithea with SSH
92 Using Kallithea with SSH
@@ -129,23 +133,23 b' Kallithea provides full text search of r'
129
133
130 For an incremental index build, run::
134 For an incremental index build, run::
131
135
132 paster make-index my.ini
136 kallithea-cli index-create -c my.ini
133
137
134 For a full index rebuild, run::
138 For a full index rebuild, run::
135
139
136 paster make-index my.ini -f
140 kallithea-cli index-create -c my.ini --full
137
141
138 The ``--repo-location`` option allows the location of the repositories to be overriden;
142 The ``--repo-location`` option allows the location of the repositories to be overridden;
139 usually, the location is retrieved from the Kallithea database.
143 usually, the location is retrieved from the Kallithea database.
140
144
141 The ``--index-only`` option can be used to limit the indexed repositories to a comma-separated list::
145 The ``--index-only`` option can be used to limit the indexed repositories to a comma-separated list::
142
146
143 paster make-index my.ini --index-only=vcs,kallithea
147 kallithea-cli index-create -c my.ini --index-only=vcs,kallithea
144
148
145 To keep your index up-to-date it is necessary to do periodic index builds;
149 To keep your index up-to-date it is necessary to do periodic index builds;
146 for this, it is recommended to use a crontab entry. Example::
150 for this, it is recommended to use a crontab entry. Example::
147
151
148 0 3 * * * /path/to/virtualenv/bin/paster make-index /path/to/kallithea/my.ini
152 0 3 * * * /path/to/virtualenv/bin/kallithea-cli index-create -c /path/to/kallithea/my.ini
149
153
150 When using incremental mode (the default), Whoosh will check the last
154 When using incremental mode (the default), Whoosh will check the last
151 modification date of each file and add it to be reindexed if a newer file is
155 modification date of each file and add it to be reindexed if a newer file is
@@ -155,321 +159,75 b' from index.'
155 If you want to rebuild the index from scratch, you can use the ``-f`` flag as above,
159 If you want to rebuild the index from scratch, you can use the ``-f`` flag as above,
156 or in the admin panel you can check the "build from scratch" checkbox.
160 or in the admin panel you can check the "build from scratch" checkbox.
157
161
158 .. _ldap-setup:
159
160 Setting up LDAP support
161 -----------------------
162
163 Kallithea supports LDAP authentication. In order
164 to use LDAP, you have to install the python-ldap_ package. This package is
165 available via PyPI, so you can install it by running::
166
167 pip install python-ldap
168
169 .. note:: ``python-ldap`` requires some libraries to be installed on
170 your system, so before installing it check that you have at
171 least the ``openldap`` and ``sasl`` libraries.
172
173 Choose *Admin > Authentication*, click the ``kallithea.lib.auth_modules.auth_ldap`` button
174 and then *Save*, to enable the LDAP plugin and configure its settings.
175
176 Here's a typical LDAP setup::
177
178 Connection settings
179 Enable LDAP = checked
180 Host = host.example.com
181 Port = 389
182 Account = <account>
183 Password = <password>
184 Connection Security = LDAPS connection
185 Certificate Checks = DEMAND
186
187 Search settings
188 Base DN = CN=users,DC=host,DC=example,DC=org
189 LDAP Filter = (&(objectClass=user)(!(objectClass=computer)))
190 LDAP Search Scope = SUBTREE
191
192 Attribute mappings
193 Login Attribute = uid
194 First Name Attribute = firstName
195 Last Name Attribute = lastName
196 Email Attribute = mail
197
198 If your user groups are placed in an Organisation Unit (OU) structure, the Search Settings configuration differs::
199
200 Search settings
201 Base DN = DC=host,DC=example,DC=org
202 LDAP Filter = (&(memberOf=CN=your user group,OU=subunit,OU=unit,DC=host,DC=example,DC=org)(objectClass=user))
203 LDAP Search Scope = SUBTREE
204
205 .. _enable_ldap:
206
207 Enable LDAP : required
208 Whether to use LDAP for authenticating users.
209
210 .. _ldap_host:
211
212 Host : required
213 LDAP server hostname or IP address. Can be also a comma separated
214 list of servers to support LDAP fail-over.
215
216 .. _Port:
217
218 Port : required
219 389 for un-encrypted LDAP, 636 for SSL-encrypted LDAP.
220
221 .. _ldap_account:
222
223 Account : optional
224 Only required if the LDAP server does not allow anonymous browsing of
225 records. This should be a special account for record browsing. This
226 will require `LDAP Password`_ below.
227
228 .. _LDAP Password:
229
230 Password : optional
231 Only required if the LDAP server does not allow anonymous browsing of
232 records.
233
234 .. _Enable LDAPS:
235
236 Connection Security : required
237 Defines the connection to LDAP server
238
239 No encryption
240 Plain non encrypted connection
241
242 LDAPS connection
243 Enable LDAPS connections. It will likely require `Port`_ to be set to
244 a different value (standard LDAPS port is 636). When LDAPS is enabled
245 then `Certificate Checks`_ is required.
246
247 START_TLS on LDAP connection
248 START TLS connection
249
250 .. _Certificate Checks:
251
252 Certificate Checks : optional
253 How SSL certificates verification is handled -- this is only useful when
254 `Enable LDAPS`_ is enabled. Only DEMAND or HARD offer full SSL security
255 while the other options are susceptible to man-in-the-middle attacks. SSL
256 certificates can be installed to /etc/openldap/cacerts so that the
257 DEMAND or HARD options can be used with self-signed certificates or
258 certificates that do not have traceable certificates of authority.
259
260 NEVER
261 A serve certificate will never be requested or checked.
262
263 ALLOW
264 A server certificate is requested. Failure to provide a
265 certificate or providing a bad certificate will not terminate the
266 session.
267
268 TRY
269 A server certificate is requested. Failure to provide a
270 certificate does not halt the session; providing a bad certificate
271 halts the session.
272
273 DEMAND
274 A server certificate is requested and must be provided and
275 authenticated for the session to proceed.
276
277 HARD
278 The same as DEMAND.
279
280 .. _Base DN:
281
282 Base DN : required
283 The Distinguished Name (DN) where searches for users will be performed.
284 Searches can be controlled by `LDAP Filter`_ and `LDAP Search Scope`_.
285
286 .. _LDAP Filter:
287
288 LDAP Filter : optional
289 A LDAP filter defined by RFC 2254. This is more useful when `LDAP
290 Search Scope`_ is set to SUBTREE. The filter is useful for limiting
291 which LDAP objects are identified as representing Users for
292 authentication. The filter is augmented by `Login Attribute`_ below.
293 This can commonly be left blank.
294
295 .. _LDAP Search Scope:
296
297 LDAP Search Scope : required
298 This limits how far LDAP will search for a matching object.
299
300 BASE
301 Only allows searching of `Base DN`_ and is usually not what you
302 want.
303
304 ONELEVEL
305 Searches all entries under `Base DN`_, but not Base DN itself.
306
307 SUBTREE
308 Searches all entries below `Base DN`_, but not Base DN itself.
309 When using SUBTREE `LDAP Filter`_ is useful to limit object
310 location.
311
312 .. _Login Attribute:
313
314 Login Attribute : required
315 The LDAP record attribute that will be matched as the USERNAME or
316 ACCOUNT used to connect to Kallithea. This will be added to `LDAP
317 Filter`_ for locating the User object. If `LDAP Filter`_ is specified as
318 "LDAPFILTER", `Login Attribute`_ is specified as "uid" and the user has
319 connected as "jsmith" then the `LDAP Filter`_ will be augmented as below
320 ::
321
322 (&(LDAPFILTER)(uid=jsmith))
323
324 .. _ldap_attr_firstname:
325
326 First Name Attribute : required
327 The LDAP record attribute which represents the user's first name.
328
329 .. _ldap_attr_lastname:
330
331 Last Name Attribute : required
332 The LDAP record attribute which represents the user's last name.
333
334 .. _ldap_attr_email:
335
336 Email Attribute : required
337 The LDAP record attribute which represents the user's email address.
338
339 If all data are entered correctly, and python-ldap_ is properly installed
340 users should be granted access to Kallithea with LDAP accounts. At this
341 time user information is copied from LDAP into the Kallithea user database.
342 This means that updates of an LDAP user object may not be reflected as a
343 user update in Kallithea.
344
345 If You have problems with LDAP access and believe You entered correct
346 information check out the Kallithea logs, any error messages sent from LDAP
347 will be saved there.
348
349 Active Directory
350 ''''''''''''''''
351
352 Kallithea can use Microsoft Active Directory for user authentication. This
353 is done through an LDAP or LDAPS connection to Active Directory. The
354 following LDAP configuration settings are typical for using Active
355 Directory ::
356
357 Base DN = OU=SBSUsers,OU=Users,OU=MyBusiness,DC=v3sys,DC=local
358 Login Attribute = sAMAccountName
359 First Name Attribute = givenName
360 Last Name Attribute = sn
361 Email Attribute = mail
362
363 All other LDAP settings will likely be site-specific and should be
364 appropriately configured.
365
366
367 Authentication by container or reverse-proxy
368 --------------------------------------------
369
370 Kallithea supports delegating the authentication
371 of users to its WSGI container, or to a reverse-proxy server through which all
372 clients access the application.
373
374 When these authentication methods are enabled in Kallithea, it uses the
375 username that the container/proxy (Apache or Nginx, etc.) provides and doesn't
376 perform the authentication itself. The authorization, however, is still done by
377 Kallithea according to its settings.
378
379 When a user logs in for the first time using these authentication methods,
380 a matching user account is created in Kallithea with default permissions. An
381 administrator can then modify it using Kallithea's admin interface.
382
383 It's also possible for an administrator to create accounts and configure their
384 permissions before the user logs in for the first time, using the :ref:`create-user` API.
385
386 Container-based authentication
387 ''''''''''''''''''''''''''''''
388
389 In a container-based authentication setup, Kallithea reads the user name from
390 the ``REMOTE_USER`` server variable provided by the WSGI container.
391
392 After setting up your container (see `Apache with mod_wsgi`_), you'll need
393 to configure it to require authentication on the location configured for
394 Kallithea.
395
396 Proxy pass-through authentication
397 '''''''''''''''''''''''''''''''''
398
399 In a proxy pass-through authentication setup, Kallithea reads the user name
400 from the ``X-Forwarded-User`` request header, which should be configured to be
401 sent by the reverse-proxy server.
402
403 After setting up your proxy solution (see `Apache virtual host reverse proxy example`_,
404 `Apache as subdirectory`_ or `Nginx virtual host example`_), you'll need to
405 configure the authentication and add the username in a request header named
406 ``X-Forwarded-User``.
407
408 For example, the following config section for Apache sets a subdirectory in a
409 reverse-proxy setup with basic auth:
410
411 .. code-block:: apache
412
413 <Location /someprefix>
414 ProxyPass http://127.0.0.1:5000/someprefix
415 ProxyPassReverse http://127.0.0.1:5000/someprefix
416 SetEnvIf X-Url-Scheme https HTTPS=1
417
418 AuthType Basic
419 AuthName "Kallithea authentication"
420 AuthUserFile /srv/kallithea/.htpasswd
421 Require valid-user
422
423 RequestHeader unset X-Forwarded-User
424
425 RewriteEngine On
426 RewriteCond %{LA-U:REMOTE_USER} (.+)
427 RewriteRule .* - [E=RU:%1]
428 RequestHeader set X-Forwarded-User %{RU}e
429 </Location>
430
431 .. note::
432 If you enable proxy pass-through authentication, make sure your server is
433 only accessible through the proxy. Otherwise, any client would be able to
434 forge the authentication header and could effectively become authenticated
435 using any account of their liking.
436
437
162
438 Integration with issue trackers
163 Integration with issue trackers
439 -------------------------------
164 -------------------------------
440
165
441 Kallithea provides a simple integration with issue trackers. It's possible
166 Kallithea provides a simple integration with issue trackers. It's possible
442 to define a regular expression that will match an issue ID in commit messages,
167 to define a regular expression that will match an issue ID in commit messages,
443 and have that replaced with a URL to the issue. To enable this simply
168 and have that replaced with a URL to the issue.
444 uncomment the following variables in the ini file::
169
170 This is achieved with following three variables in the ini file::
445
171
446 issue_pat = (?:^#|\s#)(\w+)
172 issue_pat = #(\d+)
447 issue_server_link = https://issues.example.com/{repo}/issue/{id}
173 issue_server_link = https://issues.example.com/{repo}/issue/\1
448 issue_prefix = #
174 issue_sub =
449
175
450 ``issue_pat`` is the regular expression describing which strings in
176 ``issue_pat`` is the regular expression describing which strings in
451 commit messages will be treated as issue references. A match group in
177 commit messages will be treated as issue references. The expression can/should
452 parentheses should be used to specify the actual issue id.
178 have one or more parenthesized groups that can later be referred to in
179 ``issue_server_link`` and ``issue_sub`` (see below). If you prefer, named groups
180 can be used instead of simple parenthesized groups.
453
181
454 The default expression matches issues in the format ``#<number>``, e.g., ``#300``.
182 If the pattern should only match if it is preceded by whitespace, add the
183 following string before the actual pattern: ``(?:^|(?<=\s))``.
184 If the pattern should only match if it is followed by whitespace, add the
185 following string after the actual pattern: ``(?:$|(?=\s))``.
186 These expressions use lookbehind and lookahead assertions of the Python regular
187 expression module to avoid the whitespace to be part of the actual pattern,
188 otherwise the link text will also contain that whitespace.
455
189
456 Matched issue references are replaced with the link specified in
190 Matched issue references are replaced with the link specified in
457 ``issue_server_link``. ``{id}`` is replaced with the issue ID, and
191 ``issue_server_link``, in which any backreferences are resolved. Backreferences
458 ``{repo}`` with the repository name. Since the # is stripped away,
192 can be ``\1``, ``\2``, ... or for named groups ``\g<groupname>``.
459 ``issue_prefix`` is prepended to the link text. ``issue_prefix`` doesn't
193 The special token ``{repo}`` is replaced with the full repository path
460 necessarily need to be ``#``: if you set issue prefix to ``ISSUE-`` this will
194 (including repository groups), while token ``{repo_name}`` is replaced with the
461 generate a URL in the format:
195 repository name (without repository groups).
196
197 The link text is determined by ``issue_sub``, which can be a string containing
198 backreferences to the groups specified in ``issue_pat``. If ``issue_sub`` is
199 empty, then the text matched by ``issue_pat`` is used verbatim.
200
201 The example settings shown above match issues in the format ``#<number>``.
202 This will cause the text ``#300`` to be transformed into a link:
462
203
463 .. code-block:: html
204 .. code-block:: html
464
205
465 <a href="https://issues.example.com/example_repo/issue/300">ISSUE-300</a>
206 <a href="https://issues.example.com/example_repo/issue/300">#300</a>
207
208 The following example transforms a text starting with either of 'pullrequest',
209 'pull request' or 'PR', followed by an optional space, then a pound character
210 (#) and one or more digits, into a link with the text 'PR #' followed by the
211 digits::
212
213 issue_pat = (pullrequest|pull request|PR) ?#(\d+)
214 issue_server_link = https://issues.example.com/\2
215 issue_sub = PR #\2
216
217 The following example demonstrates how to require whitespace before the issue
218 reference in order for it to be recognized, such that the text ``issue#123`` will
219 not cause a match, but ``issue #123`` will::
220
221 issue_pat = (?:^|(?<=\s))#(\d+)
222 issue_server_link = https://issues.example.com/\1
223 issue_sub =
466
224
467 If needed, more than one pattern can be specified by appending a unique suffix to
225 If needed, more than one pattern can be specified by appending a unique suffix to
468 the variables. For example::
226 the variables. For example, also demonstrating the use of named groups::
469
227
470 issue_pat_wiki = (?:wiki-)(.+)
228 issue_pat_wiki = wiki-(?P<pagename>\S+)
471 issue_server_link_wiki = https://wiki.example.com/{id}
229 issue_server_link_wiki = https://wiki.example.com/\g<pagename>
472 issue_prefix_wiki = WIKI-
230 issue_sub_wiki = WIKI-\g<pagename>
473
231
474 With these settings, wiki pages can be referenced as wiki-some-id, and every
232 With these settings, wiki pages can be referenced as wiki-some-id, and every
475 such reference will be transformed into:
233 such reference will be transformed into:
@@ -478,6 +236,9 b' such reference will be transformed into:'
478
236
479 <a href="https://wiki.example.com/some-id">WIKI-some-id</a>
237 <a href="https://wiki.example.com/some-id">WIKI-some-id</a>
480
238
239 Refer to the `Python regular expression documentation`_ for more details about
240 the supported syntax in ``issue_pat``, ``issue_server_link`` and ``issue_sub``.
241
481
242
482 Hook management
243 Hook management
483 ---------------
244 ---------------
@@ -502,6 +263,9 b' encoding of commit messages. In addition'
502 library is installed. If ``chardet`` is detected Kallithea will fallback to it
263 library is installed. If ``chardet`` is detected Kallithea will fallback to it
503 when there are encode/decode errors.
264 when there are encode/decode errors.
504
265
266 The Mercurial encoding is configurable as ``hgencoding``. It is similar to
267 setting the ``HGENCODING`` environment variable, but will override it.
268
505
269
506 Celery configuration
270 Celery configuration
507 --------------------
271 --------------------
@@ -531,7 +295,10 b' Celery. So for example setting `BROKER_H'
531
295
532 To start the Celery process, run::
296 To start the Celery process, run::
533
297
534 paster celeryd <configfile.ini>
298 kallithea-cli celery-run -c my.ini
299
300 Extra options to the Celery worker can be passed after ``--`` - see ``-- -h``
301 for more info.
535
302
536 .. note::
303 .. note::
537 Make sure you run this command from the same virtualenv, and with the same
304 Make sure you run this command from the same virtualenv, and with the same
@@ -552,6 +319,8 b' directly which scheme/protocol Kallithea'
552 - With ``force_https = true`` the default will be ``https``.
319 - With ``force_https = true`` the default will be ``https``.
553 - With ``use_htsts = true``, Kallithea will set ``Strict-Transport-Security`` when using https.
320 - With ``use_htsts = true``, Kallithea will set ``Strict-Transport-Security`` when using https.
554
321
322 .. _nginx_virtual_host:
323
555
324
556 Nginx virtual host example
325 Nginx virtual host example
557 --------------------------
326 --------------------------
@@ -606,7 +375,7 b' Sample config for Nginx using proxy:'
606
375
607 ## uncomment root directive if you want to serve static files by nginx
376 ## uncomment root directive if you want to serve static files by nginx
608 ## requires static_files = false in .ini file
377 ## requires static_files = false in .ini file
609 #root /path/to/installation/kallithea/public;
378 #root /srv/kallithea/kallithea/kallithea/public;
610 include /etc/nginx/proxy.conf;
379 include /etc/nginx/proxy.conf;
611 location / {
380 location / {
612 try_files $uri @kallithea;
381 try_files $uri @kallithea;
@@ -640,6 +409,8 b' pushes or large pushes::'
640 client_body_buffer_size 128k;
409 client_body_buffer_size 128k;
641 large_client_header_buffers 8 64k;
410 large_client_header_buffers 8 64k;
642
411
412 .. _apache_virtual_host_reverse_proxy:
413
643
414
644 Apache virtual host reverse proxy example
415 Apache virtual host reverse proxy example
645 -----------------------------------------
416 -----------------------------------------
@@ -661,7 +432,7 b' Here is a sample configuration file for '
661 </Proxy>
432 </Proxy>
662
433
663 #important !
434 #important !
664 #Directive to properly generate url (clone url) for pylons
435 #Directive to properly generate url (clone url) for Kallithea
665 ProxyPreserveHost On
436 ProxyPreserveHost On
666
437
667 #kallithea instance
438 #kallithea instance
@@ -675,6 +446,8 b' Here is a sample configuration file for '
675 Additional tutorial
446 Additional tutorial
676 http://pylonsbook.com/en/1.1/deployment.html#using-apache-to-proxy-requests-to-pylons
447 http://pylonsbook.com/en/1.1/deployment.html#using-apache-to-proxy-requests-to-pylons
677
448
449 .. _apache_subdirectory:
450
678
451
679 Apache as subdirectory
452 Apache as subdirectory
680 ----------------------
453 ----------------------
@@ -683,9 +456,9 b' Apache subdirectory part:'
683
456
684 .. code-block:: apache
457 .. code-block:: apache
685
458
686 <Location /<someprefix> >
459 <Location /PREFIX >
687 ProxyPass http://127.0.0.1:5000/<someprefix>
460 ProxyPass http://127.0.0.1:5000/PREFIX
688 ProxyPassReverse http://127.0.0.1:5000/<someprefix>
461 ProxyPassReverse http://127.0.0.1:5000/PREFIX
689 SetEnvIf X-Url-Scheme https HTTPS=1
462 SetEnvIf X-Url-Scheme https HTTPS=1
690 </Location>
463 </Location>
691
464
@@ -698,9 +471,11 b' Add the following at the end of the .ini'
698
471
699 [filter:proxy-prefix]
472 [filter:proxy-prefix]
700 use = egg:PasteDeploy#prefix
473 use = egg:PasteDeploy#prefix
701 prefix = /<someprefix>
474 prefix = /PREFIX
702
475
703 then change ``<someprefix>`` into your chosen prefix
476 then change ``PREFIX`` into your chosen prefix
477
478 .. _apache_mod_wsgi:
704
479
705
480
706 Apache with mod_wsgi
481 Apache with mod_wsgi
@@ -755,27 +530,21 b' usually ``www-data`` or ``apache``. If y'
755 directory owned by a different user, use the user and group options to
530 directory owned by a different user, use the user and group options to
756 WSGIDaemonProcess to set the name of the user and group.
531 WSGIDaemonProcess to set the name of the user and group.
757
532
758 .. note::
759 If running Kallithea in multiprocess mode,
760 make sure you set ``instance_id = *`` in the configuration so each process
761 gets it's own cache invalidation key.
762
763 Example WSGI dispatch script:
533 Example WSGI dispatch script:
764
534
765 .. code-block:: python
535 .. code-block:: python
766
536
767 import os
537 import os
768 os.environ["HGENCODING"] = "UTF-8"
769 os.environ['PYTHON_EGG_CACHE'] = '/srv/kallithea/.egg-cache'
538 os.environ['PYTHON_EGG_CACHE'] = '/srv/kallithea/.egg-cache'
770
539
771 # sometimes it's needed to set the curent dir
540 # sometimes it's needed to set the current dir
772 os.chdir('/srv/kallithea/')
541 os.chdir('/srv/kallithea/')
773
542
774 import site
543 import site
775 site.addsitedir("/srv/kallithea/venv/lib/python2.7/site-packages")
544 site.addsitedir("/srv/kallithea/venv/lib/python2.7/site-packages")
776
545
777 ini = '/srv/kallithea/my.ini'
546 ini = '/srv/kallithea/my.ini'
778 from paste.script.util.logging_config import fileConfig
547 from logging.config import fileConfig
779 fileConfig(ini)
548 fileConfig(ini)
780 from paste.deploy import loadapp
549 from paste.deploy import loadapp
781 application = loadapp('config:' + ini)
550 application = loadapp('config:' + ini)
@@ -791,7 +560,7 b' Or using proper virtualenv activation:'
791 os.environ['HOME'] = '/srv/kallithea'
560 os.environ['HOME'] = '/srv/kallithea'
792
561
793 ini = '/srv/kallithea/kallithea.ini'
562 ini = '/srv/kallithea/kallithea.ini'
794 from paste.script.util.logging_config import fileConfig
563 from logging.config import fileConfig
795 fileConfig(ini)
564 fileConfig(ini)
796 from paste.deploy import loadapp
565 from paste.deploy import loadapp
797 application = loadapp('config:' + ini)
566 application = loadapp('config:' + ini)
@@ -808,11 +577,11 b' the ``init.d`` directory of the Kallithe'
808
577
809 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
578 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
810 .. _python: http://www.python.org/
579 .. _python: http://www.python.org/
580 .. _Python regular expression documentation: https://docs.python.org/2/library/re.html
811 .. _Mercurial: https://www.mercurial-scm.org/
581 .. _Mercurial: https://www.mercurial-scm.org/
812 .. _Celery: http://celeryproject.org/
582 .. _Celery: http://celeryproject.org/
813 .. _Celery documentation: http://docs.celeryproject.org/en/latest/getting-started/index.html
583 .. _Celery documentation: http://docs.celeryproject.org/en/latest/getting-started/index.html
814 .. _RabbitMQ: http://www.rabbitmq.com/
584 .. _RabbitMQ: http://www.rabbitmq.com/
815 .. _Redis: http://redis.io/
585 .. _Redis: http://redis.io/
816 .. _python-ldap: http://www.python-ldap.org/
817 .. _mercurial-server: http://www.lshift.net/mercurial-server.html
586 .. _mercurial-server: http://www.lshift.net/mercurial-server.html
818 .. _PublishingRepositories: https://www.mercurial-scm.org/wiki/PublishingRepositories
587 .. _PublishingRepositories: https://www.mercurial-scm.org/wiki/PublishingRepositories
@@ -2,11 +2,11 b''
2 * Sphinx stylesheet -- default theme
2 * Sphinx stylesheet -- default theme
3 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 */
4 */
5
5
6 @import url("basic.css");
6 @import url("basic.css");
7
7
8 /* -- page layout ----------------------------------------------------------- */
8 /* -- page layout ----------------------------------------------------------- */
9
9
10 body {
10 body {
11 font-family: Arial, sans-serif;
11 font-family: Arial, sans-serif;
12 font-size: 100%;
12 font-size: 100%;
@@ -28,18 +28,18 b' div.bodywrapper {'
28 hr{
28 hr{
29 border: 1px solid #B1B4B6;
29 border: 1px solid #B1B4B6;
30 }
30 }
31
31
32 div.document {
32 div.document {
33 background-color: #eee;
33 background-color: #eee;
34 }
34 }
35
35
36 div.body {
36 div.body {
37 background-color: #ffffff;
37 background-color: #ffffff;
38 color: #3E4349;
38 color: #3E4349;
39 padding: 0 30px 30px 30px;
39 padding: 0 30px 30px 30px;
40 font-size: 0.8em;
40 font-size: 0.8em;
41 }
41 }
42
42
43 div.footer {
43 div.footer {
44 color: #555;
44 color: #555;
45 width: 100%;
45 width: 100%;
@@ -47,12 +47,12 b' div.footer {'
47 text-align: center;
47 text-align: center;
48 font-size: 75%;
48 font-size: 75%;
49 }
49 }
50
50
51 div.footer a {
51 div.footer a {
52 color: #444;
52 color: #444;
53 text-decoration: underline;
53 text-decoration: underline;
54 }
54 }
55
55
56 div.related {
56 div.related {
57 background-color: #577632;
57 background-color: #577632;
58 line-height: 32px;
58 line-height: 32px;
@@ -60,11 +60,11 b' div.related {'
60 text-shadow: 0px 1px 0 #444;
60 text-shadow: 0px 1px 0 #444;
61 font-size: 0.80em;
61 font-size: 0.80em;
62 }
62 }
63
63
64 div.related a {
64 div.related a {
65 color: #E2F3CC;
65 color: #E2F3CC;
66 }
66 }
67
67
68 div.sphinxsidebar {
68 div.sphinxsidebar {
69 font-size: 0.75em;
69 font-size: 0.75em;
70 line-height: 1.5em;
70 line-height: 1.5em;
@@ -73,7 +73,7 b' div.sphinxsidebar {'
73 div.sphinxsidebarwrapper{
73 div.sphinxsidebarwrapper{
74 padding: 20px 0;
74 padding: 20px 0;
75 }
75 }
76
76
77 div.sphinxsidebar h3,
77 div.sphinxsidebar h3,
78 div.sphinxsidebar h4 {
78 div.sphinxsidebar h4 {
79 font-family: Arial, sans-serif;
79 font-family: Arial, sans-serif;
@@ -89,30 +89,29 b' div.sphinxsidebar h4 {'
89 div.sphinxsidebar h4{
89 div.sphinxsidebar h4{
90 font-size: 1.1em;
90 font-size: 1.1em;
91 }
91 }
92
92
93 div.sphinxsidebar h3 a {
93 div.sphinxsidebar h3 a {
94 color: #444;
94 color: #444;
95 }
95 }
96
96
97
98 div.sphinxsidebar p {
97 div.sphinxsidebar p {
99 color: #888;
98 color: #888;
100 padding: 5px 20px;
99 padding: 5px 20px;
101 }
100 }
102
101
103 div.sphinxsidebar p.topless {
102 div.sphinxsidebar p.topless {
104 }
103 }
105
104
106 div.sphinxsidebar ul {
105 div.sphinxsidebar ul {
107 margin: 10px 20px;
106 margin: 10px 20px;
108 padding: 0;
107 padding: 0;
109 color: #000;
108 color: #000;
110 }
109 }
111
110
112 div.sphinxsidebar a {
111 div.sphinxsidebar a {
113 color: #444;
112 color: #444;
114 }
113 }
115
114
116 div.sphinxsidebar input {
115 div.sphinxsidebar input {
117 border: 1px solid #ccc;
116 border: 1px solid #ccc;
118 font-family: sans-serif;
117 font-family: sans-serif;
@@ -126,19 +125,19 b' div.sphinxsidebar input[type=text]{'
126 div.sphinxsidebar input[type=image] {
125 div.sphinxsidebar input[type=image] {
127 border: 0;
126 border: 0;
128 }
127 }
129
128
130 /* -- body styles ----------------------------------------------------------- */
129 /* -- body styles ----------------------------------------------------------- */
131
130
132 a {
131 a {
133 color: #005B81;
132 color: #005B81;
134 text-decoration: none;
133 text-decoration: none;
135 }
134 }
136
135
137 a:hover {
136 a:hover {
138 color: #E32E00;
137 color: #E32E00;
139 text-decoration: underline;
138 text-decoration: underline;
140 }
139 }
141
140
142 div.body h1,
141 div.body h1,
143 div.body h2,
142 div.body h2,
144 div.body h3,
143 div.body h3,
@@ -153,30 +152,30 b' div.body h6 {'
153 padding: 5px 0 5px 10px;
152 padding: 5px 0 5px 10px;
154 text-shadow: 0px 1px 0 white
153 text-shadow: 0px 1px 0 white
155 }
154 }
156
155
157 div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; }
156 div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; }
158 div.body h2 { font-size: 150%; background-color: #C8D5E3; }
157 div.body h2 { font-size: 150%; background-color: #C8D5E3; }
159 div.body h3 { font-size: 120%; background-color: #D8DEE3; }
158 div.body h3 { font-size: 120%; background-color: #D8DEE3; }
160 div.body h4 { font-size: 110%; background-color: #D8DEE3; }
159 div.body h4 { font-size: 110%; background-color: #D8DEE3; }
161 div.body h5 { font-size: 100%; background-color: #D8DEE3; }
160 div.body h5 { font-size: 100%; background-color: #D8DEE3; }
162 div.body h6 { font-size: 100%; background-color: #D8DEE3; }
161 div.body h6 { font-size: 100%; background-color: #D8DEE3; }
163
162
164 a.headerlink {
163 a.headerlink {
165 color: #c60f0f;
164 color: #c60f0f;
166 font-size: 0.8em;
165 font-size: 0.8em;
167 padding: 0 4px 0 4px;
166 padding: 0 4px 0 4px;
168 text-decoration: none;
167 text-decoration: none;
169 }
168 }
170
169
171 a.headerlink:hover {
170 a.headerlink:hover {
172 background-color: #c60f0f;
171 background-color: #c60f0f;
173 color: white;
172 color: white;
174 }
173 }
175
174
176 div.body p, div.body dd, div.body li {
175 div.body p, div.body dd, div.body li {
177 line-height: 1.5em;
176 line-height: 1.5em;
178 }
177 }
179
178
180 div.admonition p.admonition-title + p {
179 div.admonition p.admonition-title + p {
181 display: inline;
180 display: inline;
182 }
181 }
@@ -189,29 +188,29 b' div.note {'
189 background-color: #eee;
188 background-color: #eee;
190 border: 1px solid #ccc;
189 border: 1px solid #ccc;
191 }
190 }
192
191
193 div.seealso {
192 div.seealso {
194 background-color: #ffc;
193 background-color: #ffc;
195 border: 1px solid #ff6;
194 border: 1px solid #ff6;
196 }
195 }
197
196
198 div.topic {
197 div.topic {
199 background-color: #eee;
198 background-color: #eee;
200 }
199 }
201
200
202 div.warning {
201 div.warning {
203 background-color: #ffe4e4;
202 background-color: #ffe4e4;
204 border: 1px solid #f66;
203 border: 1px solid #f66;
205 }
204 }
206
205
207 p.admonition-title {
206 p.admonition-title {
208 display: inline;
207 display: inline;
209 }
208 }
210
209
211 p.admonition-title:after {
210 p.admonition-title:after {
212 content: ":";
211 content: ":";
213 }
212 }
214
213
215 pre {
214 pre {
216 padding: 10px;
215 padding: 10px;
217 background-color: White;
216 background-color: White;
@@ -222,7 +221,7 b' pre {'
222 margin: 1.5em 0 1.5em 0;
221 margin: 1.5em 0 1.5em 0;
223 box-shadow: 1px 1px 1px #d8d8d8;
222 box-shadow: 1px 1px 1px #d8d8d8;
224 }
223 }
225
224
226 tt {
225 tt {
227 background-color: #ecf0f3;
226 background-color: #ecf0f3;
228 color: #222;
227 color: #222;
@@ -25,7 +25,7 b' Enable interactive debug mode'
25
25
26 To enable interactive debug mode simply comment out ``set debug = false`` in
26 To enable interactive debug mode simply comment out ``set debug = false`` in
27 the .ini file. This will trigger an interactive debugger each time
27 the .ini file. This will trigger an interactive debugger each time
28 there is an error in the browser, or send a http link if an error occured in the backend. This
28 there is an error in the browser, or send a http link if an error occurred in the backend. This
29 is a great tool for fast debugging as you get a handy Python console right
29 is a great tool for fast debugging as you get a handy Python console right
30 in the web view.
30 in the web view.
31
31
@@ -12,8 +12,17 b' cannot be sent, all mails will show up i'
12 Before any email can be sent, an SMTP server has to be configured using the
12 Before any email can be sent, an SMTP server has to be configured using the
13 configuration file setting ``smtp_server``. If required for that server, specify
13 configuration file setting ``smtp_server``. If required for that server, specify
14 a username (``smtp_username``) and password (``smtp_password``), a non-standard
14 a username (``smtp_username``) and password (``smtp_password``), a non-standard
15 port (``smtp_port``), encryption settings (``smtp_use_tls`` or ``smtp_use_ssl``)
15 port (``smtp_port``), whether to use "SSL" when connecting (``smtp_use_ssl``)
16 and/or specific authentication parameters (``smtp_auth``).
16 or use STARTTLS (``smtp_use_tls``), and/or specify special ESMTP "auth" features
17 (``smtp_auth``).
18
19 For example, for sending through gmail, use::
20
21 smtp_server = smtp.gmail.com
22 smtp_username = username
23 smtp_password = password
24 smtp_port = 465
25 smtp_use_ssl = true
17
26
18
27
19 Application emails
28 Application emails
@@ -67,9 +76,8 b' Error emails'
67
76
68 When an exception occurs in Kallithea -- and unless interactive debugging is
77 When an exception occurs in Kallithea -- and unless interactive debugging is
69 enabled using ``set debug = true`` in the ``[app:main]`` section of the
78 enabled using ``set debug = true`` in the ``[app:main]`` section of the
70 configuration file -- an email with exception details is sent by WebError_'s
79 configuration file -- an email with exception details is sent by backlash_
71 ``ErrorMiddleware`` to the addresses specified in ``email_to`` in the
80 to the addresses specified in ``email_to`` in the configuration file.
72 configuration file.
73
81
74 Recipients will see these emails originating from the sender specified in the
82 Recipients will see these emails originating from the sender specified in the
75 ``error_email_from`` setting in the configuration file. This setting can either
83 ``error_email_from`` setting in the configuration file. This setting can either
@@ -77,10 +85,6 b' contain only an email address, like `kal'
77 a name and an address in the following format: `Kallithea Errors
85 a name and an address in the following format: `Kallithea Errors
78 <kallithea-noreply@example.com>`.
86 <kallithea-noreply@example.com>`.
79
87
80 *Note:* The WebError_ package does not respect ``smtp_port`` and assumes the
81 standard SMTP port (25). If you have a remote SMTP server with a different port,
82 you could set up a local forwarding SMTP server on port 25.
83
84
88
85 References
89 References
86 ----------
90 ----------
@@ -89,4 +93,4 b' References'
89 - `ErrorHandler (Pylons modules documentation) <http://pylons-webframework.readthedocs.org/en/latest/modules/middleware.html#pylons.middleware.ErrorHandler>`_
93 - `ErrorHandler (Pylons modules documentation) <http://pylons-webframework.readthedocs.org/en/latest/modules/middleware.html#pylons.middleware.ErrorHandler>`_
90
94
91
95
92 .. _WebError: https://pypi.python.org/pypi/WebError
96 .. _backlash: https://github.com/TurboGears/backlash
@@ -8,18 +8,18 b' General Kallithea usage'
8 Repository deletion
8 Repository deletion
9 -------------------
9 -------------------
10
10
11 Currently when an admin or owner deletes a repository, Kallithea does
11 When an admin or owner deletes a repository, Kallithea does
12 not physically delete said repository from the filesystem, but instead
12 not physically delete said repository from the filesystem, but instead
13 renames it in a special way so that it is not possible to push, clone
13 renames it in a special way so that it is not possible to push, clone
14 or access the repository.
14 or access the repository.
15
15
16 There is a special command for cleaning up such archived repositories::
16 There is a special command for cleaning up such archived repositories::
17
17
18 paster cleanup-repos --older-than=30d my.ini
18 kallithea-cli repo-purge-deleted -c my.ini --older-than=30d
19
19
20 This command scans for archived repositories that are older than
20 This command scans for archived repositories that are older than
21 30 days, displays them, and asks if you want to delete them (unless given
21 30 days, displays them, and asks if you want to delete them (unless given
22 the ``--dont-ask`` flag). If you host a large amount of repositories with
22 the ``--no-ask`` flag). If you host a large amount of repositories with
23 forks that are constantly being deleted, it is recommended that you run this
23 forks that are constantly being deleted, it is recommended that you run this
24 command via crontab.
24 command via crontab.
25
25
@@ -151,7 +151,7 b' described in more detail in this documen'
151 features that merit further explanation.
151 features that merit further explanation.
152
152
153 Repository extra fields
153 Repository extra fields
154 ~~~~~~~~~~~~~~~~~~~~~~~
154 ^^^^^^^^^^^^^^^^^^^^^^^
155
155
156 In the *Visual* tab, there is an option "Use repository extra
156 In the *Visual* tab, there is an option "Use repository extra
157 fields", which allows to set custom fields for each repository in the system.
157 fields", which allows to set custom fields for each repository in the system.
@@ -165,7 +165,7 b' about a manager of each repository. The'
165 Newly created fields are accessible via the API.
165 Newly created fields are accessible via the API.
166
166
167 Meta tagging
167 Meta tagging
168 ~~~~~~~~~~~~
168 ^^^^^^^^^^^^
169
169
170 In the *Visual* tab, option "Stylify recognised meta tags" will cause Kallithea
170 In the *Visual* tab, option "Stylify recognised meta tags" will cause Kallithea
171 to turn certain text fragments in repository and repository group
171 to turn certain text fragments in repository and repository group
@@ -4,59 +4,65 b''
4 Optimizing Kallithea performance
4 Optimizing Kallithea performance
5 ================================
5 ================================
6
6
7 When serving a large amount of big repositories, Kallithea can start
7 When serving a large amount of big repositories, Kallithea can start performing
8 performing slower than expected. Because of the demanding nature of handling large
8 slower than expected. Because of the demanding nature of handling large amounts
9 amounts of data from version control systems, here are some tips on how to get
9 of data from version control systems, here are some tips on how to get the best
10 the best performance.
10 performance.
11
11
12
12 * Kallithea is often I/O bound, and hence a fast disk (SSD/SAN) is
13 Fast storage
13 usually more important than a fast CPU.
14 ------------
14
15
15 * Sluggish loading of the front page can easily be fixed by grouping repositories or by
16 Kallithea is often I/O bound, and hence a fast disk (SSD/SAN) and plenty of RAM
16 increasing cache size (see below). This includes using the lightweight dashboard
17 is usually more important than a fast CPU.
17 option and ``vcs_full_cache`` setting in .ini file.
18
18
19
19 Follow these few steps to improve performance of Kallithea system.
20 Caching
21 -------
20
22
21 1. Increase cache
23 Tweak beaker cache settings in the ini file. The actual effect of that is
24 questionable.
22
25
23 Tweak beaker cache settings in the ini file. The actual effect of that
24 is questionable.
25
26
26 2. Switch from SQLite to PostgreSQL or MySQL
27 Database
28 --------
27
29
28 SQLite is a good option when having a small load on the system. But due to
30 SQLite is a good option when having a small load on the system. But due to
29 locking issues with SQLite, it is not recommended to use it for larger
31 locking issues with SQLite, it is not recommended to use it for larger
30 deployments. Switching to MySQL or PostgreSQL will result in an immediate
32 deployments.
31 performance increase. A tool like SQLAlchemyGrate_ can be used for
32 migrating to another database platform.
33
33
34 3. Scale Kallithea horizontally
34 Switching to MySQL or PostgreSQL will result in an immediate performance
35 increase. A tool like SQLAlchemyGrate_ can be used for migrating to another
36 database platform.
37
35
38
36 Scaling horizontally can give huge performance benefits when dealing with
39 Horizontal scaling
37 large amounts of traffic (many users, CI servers, etc.). Kallithea can be
40 ------------------
38 scaled horizontally on one (recommended) or multiple machines.
39
41
40 It is generally possible to run WSGI applications multithreaded, so that
42 Scaling horizontally means running several Kallithea instances and let them
41 several HTTP requests are served from the same Python process at once. That
43 share the load. That can give huge performance benefits when dealing with large
42 can in principle give better utilization of internal caches and less
44 amounts of traffic (many users, CI servers, etc.). Kallithea can be scaled
43 process overhead.
45 horizontally on one (recommended) or multiple machines.
44
46
45 One danger of running multithreaded is that program execution becomes much
47 It is generally possible to run WSGI applications multithreaded, so that
46 more complex; programs must be written to consider all combinations of
48 several HTTP requests are served from the same Python process at once. That can
47 events and problems might depend on timing and be impossible to reproduce.
49 in principle give better utilization of internal caches and less process
50 overhead.
48
51
49 Kallithea can't promise to be thread-safe, just like the embedded Mercurial
52 One danger of running multithreaded is that program execution becomes much more
50 backend doesn't make any strong promises when used as Kallithea uses it.
53 complex; programs must be written to consider all combinations of events and
51 Instead, we recommend scaling by using multiple server processes.
54 problems might depend on timing and be impossible to reproduce.
52
55
53 Web servers with multiple worker processes (such as ``mod_wsgi`` with the
56 Kallithea can't promise to be thread-safe, just like the embedded Mercurial
54 ``WSGIDaemonProcess`` ``processes`` parameter) will work out of the box.
57 backend doesn't make any strong promises when used as Kallithea uses it.
58 Instead, we recommend scaling by using multiple server processes.
55
59
56 In order to scale horizontally on multiple machines, you need to do the
60 Web servers with multiple worker processes (such as ``mod_wsgi`` with the
57 following:
61 ``WSGIDaemonProcess`` ``processes`` parameter) will work out of the box.
58
62
59 - Each instance needs its own .ini file and unique ``instance_id`` set.
63 In order to scale horizontally on multiple machines, you need to do the
64 following:
65
60 - Each instance's ``data`` storage needs to be configured to be stored on a
66 - Each instance's ``data`` storage needs to be configured to be stored on a
61 shared disk storage, preferably together with repositories. This ``data``
67 shared disk storage, preferably together with repositories. This ``data``
62 dir contains template caches, sessions, whoosh index and is used for
68 dir contains template caches, sessions, whoosh index and is used for
@@ -71,4 +77,42 b' 3. Scale Kallithea horizontally'
71 servers or build bots.
77 servers or build bots.
72
78
73
79
80 Serve static files directly from the web server
81 -----------------------------------------------
82
83 With the default ``static_files`` ini setting, the Kallithea WSGI application
84 will take care of serving the static files from ``kallithea/public/`` at the
85 root of the application URL.
86
87 The actual serving of the static files is very fast and unlikely to be a
88 problem in a Kallithea setup - the responses generated by Kallithea from
89 database and repository content will take significantly more time and
90 resources.
91
92 To serve static files from the web server, use something like this Apache config
93 snippet::
94
95 Alias /images/ /srv/kallithea/kallithea/kallithea/public/images/
96 Alias /css/ /srv/kallithea/kallithea/kallithea/public/css/
97 Alias /js/ /srv/kallithea/kallithea/kallithea/public/js/
98 Alias /codemirror/ /srv/kallithea/kallithea/kallithea/public/codemirror/
99 Alias /fontello/ /srv/kallithea/kallithea/kallithea/public/fontello/
100
101 Then disable serving of static files in the ``.ini`` ``app:main`` section::
102
103 static_files = false
104
105 If using Kallithea installed as a package, you should be able to find the files
106 under ``site-packages/kallithea``, either in your Python installation or in your
107 virtualenv. When upgrading, make sure to update the web server configuration
108 too if necessary.
109
110 It might also be possible to improve performance by configuring the web server
111 to compress responses (served from static files or generated by Kallithea) when
112 serving them. That might also imply buffering of responses - that is more
113 likely to be a problem; large responses (clones or pulls) will have to be fully
114 processed and spooled to disk or memory before the client will see any
115 response. See the documentation for your web server.
116
117
74 .. _SQLAlchemyGrate: https://github.com/shazow/sqlalchemygrate
118 .. _SQLAlchemyGrate: https://github.com/shazow/sqlalchemygrate
@@ -63,7 +63,7 b' Troubleshooting'
63 |
63 |
64
64
65 :Q: **Requests hanging on Windows**
65 :Q: **Requests hanging on Windows**
66 :A: Please try out with disabled Antivirus software, there are some known problems with Eset Anitivirus. Make sure
66 :A: Please try out with disabled Antivirus software, there are some known problems with Eset Antivirus. Make sure
67 you have installed the latest Windows patches (especially KB2789397).
67 you have installed the latest Windows patches (especially KB2789397).
68
68
69
69
@@ -1,57 +1,80 b''
1 .. _vcs_support:
1 .. _vcs_notes:
2
3 ===============================
4 Version control systems support
5 ===============================
6
7 Kallithea supports Git and Mercurial repositories out-of-the-box.
8 For Git, you do need the ``git`` command line client installed on the server.
9
2
10 You can always disable Git or Mercurial support by editing the
3 ===================================
11 file ``kallithea/__init__.py`` and commenting out the backend.
4 Version control systems usage notes
12
5 ===================================
13 .. code-block:: python
14
6
15 BACKENDS = {
7 .. _importing:
16 'hg': 'Mercurial repository',
17 #'git': 'Git repository',
18 }
19
20
21 Git support
22 -----------
23
8
24
9
25 Web server with chunked encoding
10 Importing existing repositories
26 ````````````````````````````````
11 -------------------------------
12
13 There are two main methods to import repositories in Kallithea: via the web
14 interface or via the filesystem. If you have a large number of repositories to
15 import, importing them via the filesystem is more convenient.
16
17 Importing via web interface
18 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
19
20 For a small number of repositories, it may be easier to create the target
21 repositories through the Kallithea web interface, via *Admin > Repositories* or
22 via the *Add Repository* button on the entry page of the web interface.
27
23
28 Large Git pushes require an HTTP server with support for
24 Repositories can be nested in repository groups by first creating the group (via
29 chunked encoding for POST. The Python web servers waitress_ and
25 *Admin > Repository Groups* or via the *Add Repository Group* button on the
30 gunicorn_ (Linux only) can be used. By default, Kallithea uses
26 entry page of the web interface) and then selecting the appropriate group when
31 waitress_ for `paster serve` instead of the built-in `paste` WSGI
27 adding the repository.
32 server.
33
28
34 The paster server is controlled in the .ini file::
29 After creation of the (empty) repository, push the existing commits to the
30 *Clone URL* displayed on the repository summary page. For Git repositories,
31 first add the *Clone URL* as remote, then push the commits to that remote. The
32 specific commands to execute are shown under the *Existing repository?* section
33 of the new repository's summary page.
34
35 A benefit of this method particular for Git repositories, is that the
36 Kallithea-specific Git hooks are installed automatically. For Mercurial, no
37 hooks are required anyway.
35
38
36 use = egg:waitress#main
39 Importing via the filesystem
40 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
41
42 The alternative method of importing repositories consists of creating the
43 repositories in the desired hierarchy on the filesystem and letting Kallithea
44 scan that location.
37
45
38 or::
46 All repositories are stored in a central location on the filesystem. This
39
47 location is specified during installation (via ``db-create``) and can be reviewed
40 use = egg:gunicorn#main
48 at *Admin > Settings > VCS > Location of repositories*. Repository groups
49 (defined in *Admin > Repository Groups*) are represented by a directory in that
50 repository location. Repositories of the repository group are nested under that
51 directory.
41
52
42 Also make sure to comment out the following options::
53 To import a set of repositories and organize them in a certain repository group
54 structure, first place clones in the desired hierarchy at the configured
55 repository location.
56 These clones should be created without working directory. For Mercurial, this is
57 done with ``hg clone -U``, for Git with ``git clone --bare``.
58
59 When the repositories are added correctly on the filesystem:
43
60
44 threadpool_workers =
61 * go to *Admin > Settings > Remap and Rescan* in the Kallithea web interface
45 threadpool_max_requests =
62 * select the *Install Git hooks* checkbox when importing Git repositories
46 use_threadpool =
63 * click *Rescan Repositories*
64
65 This step will scan the filesystem and create the appropriate repository groups
66 and repositories in Kallithea.
67
68 *Note*: Once repository groups have been created this way, manage their access
69 permissions through the Kallithea web interface.
47
70
48
71
49 Mercurial support
72 Mercurial-specific notes
50 -----------------
73 ------------------------
51
74
52
75
53 Working with Mercurial subrepositories
76 Working with subrepositories
54 ``````````````````````````````````````
77 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
55
78
56 This section explains how to use Mercurial subrepositories_ in Kallithea.
79 This section explains how to use Mercurial subrepositories_ in Kallithea.
57
80
@@ -82,6 +105,4 b' Next we can edit the subrepository data,'
82 update both repositories.
105 update both repositories.
83
106
84
107
85 .. _waitress: http://pypi.python.org/pypi/waitress
86 .. _gunicorn: http://pypi.python.org/pypi/gunicorn
87 .. _subrepositories: http://mercurial.aragost.com/kick-start/en/subrepositories/
108 .. _subrepositories: http://mercurial.aragost.com/kick-start/en/subrepositories/
@@ -2,8 +2,8 b''
2 # Change variables/paths as necessary and place file /etc/init/celeryd.conf
2 # Change variables/paths as necessary and place file /etc/init/celeryd.conf
3 # start/stop/restart as normal upstart job (ie: $ start celeryd)
3 # start/stop/restart as normal upstart job (ie: $ start celeryd)
4
4
5 description "Celery for Kallithea Mercurial Server"
5 description "Celery for Kallithea Mercurial Server"
6 author "Matt Zuba <matt.zuba@goodwillaz.org"
6 author "Matt Zuba <matt.zuba@goodwillaz.org"
7
7
8 start on starting kallithea
8 start on starting kallithea
9 stop on stopped kallithea
9 stop on stopped kallithea
@@ -21,7 +21,7 b' env USER=hg'
21 # env GROUP=hg
21 # env GROUP=hg
22
22
23 script
23 script
24 COMMAND="/var/hg/.virtualenvs/kallithea/bin/paster celeryd $APPINI --pidfile=$PIDFILE"
24 COMMAND="/var/hg/.virtualenvs/kallithea/bin/kallithea-cli celery-run -c $APPINI -- --pidfile=$PIDFILE"
25 if [ -z "$GROUP" ]; then
25 if [ -z "$GROUP" ]; then
26 exec sudo -u $USER $COMMAND
26 exec sudo -u $USER $COMMAND
27 else
27 else
@@ -12,7 +12,7 b' APP_PATH="$APP_HOMEDIR/$DAEMON"'
12 CONF_NAME="production.ini"
12 CONF_NAME="production.ini"
13 LOG_FILE="/var/log/$DAEMON.log"
13 LOG_FILE="/var/log/$DAEMON.log"
14 PID_FILE="/run/daemons/$DAEMON"
14 PID_FILE="/run/daemons/$DAEMON"
15 APPL=/usr/bin/paster
15 APPL=/usr/bin/gearbox
16 RUN_AS="*****"
16 RUN_AS="*****"
17
17
18 ARGS="serve --daemon \
18 ARGS="serve --daemon \
@@ -20,7 +20,7 b' ARGS="serve --daemon \\'
20 --group=$RUN_AS \
20 --group=$RUN_AS \
21 --pid-file=$PID_FILE \
21 --pid-file=$PID_FILE \
22 --log-file=$LOG_FILE \
22 --log-file=$LOG_FILE \
23 $APP_PATH/$CONF_NAME"
23 -c $APP_PATH/$CONF_NAME"
24
24
25 [ -r /etc/conf.d/$DAEMON ] && . /etc/conf.d/$DAEMON
25 [ -r /etc/conf.d/$DAEMON ] && . /etc/conf.d/$DAEMON
26
26
@@ -47,7 +47,7 b' start)'
47 ;;
47 ;;
48 stop)
48 stop)
49 stat_busy "Stopping $DAEMON"
49 stat_busy "Stopping $DAEMON"
50 [ -n "$PID" ] && kill $PID &>/dev/null
50 [ -n "$PID" ] && kill $PID &>/dev/null
51 if [ $? = 0 ]; then
51 if [ $? = 0 ]; then
52 rm_daemon $DAEMON
52 rm_daemon $DAEMON
53 stat_done
53 stat_done
@@ -67,4 +67,4 b' status)'
67 ;;
67 ;;
68 *)
68 *)
69 echo "usage: $0 {start|stop|restart|status}"
69 echo "usage: $0 {start|stop|restart|status}"
70 esac No newline at end of file
70 esac
@@ -2,9 +2,9 b''
2 ########################################
2 ########################################
3 #### THIS IS A DEBIAN INIT.D SCRIPT ####
3 #### THIS IS A DEBIAN INIT.D SCRIPT ####
4 ########################################
4 ########################################
5
5
6 ### BEGIN INIT INFO
6 ### BEGIN INIT INFO
7 # Provides: kallithea
7 # Provides: kallithea
8 # Required-Start: $all
8 # Required-Start: $all
9 # Required-Stop: $all
9 # Required-Stop: $all
10 # Default-Start: 2 3 4 5
10 # Default-Start: 2 3 4 5
@@ -12,29 +12,29 b''
12 # Short-Description: starts instance of kallithea
12 # Short-Description: starts instance of kallithea
13 # Description: starts instance of kallithea using start-stop-daemon
13 # Description: starts instance of kallithea using start-stop-daemon
14 ### END INIT INFO
14 ### END INIT INFO
15
15
16 APP_NAME="kallithea"
16 APP_NAME="kallithea"
17 APP_HOMEDIR="opt"
17 APP_HOMEDIR="opt"
18 APP_PATH="/$APP_HOMEDIR/$APP_NAME"
18 APP_PATH="/$APP_HOMEDIR/$APP_NAME"
19
19
20 CONF_NAME="production.ini"
20 CONF_NAME="production.ini"
21
21
22 PID_PATH="$APP_PATH/$APP_NAME.pid"
22 PID_PATH="$APP_PATH/$APP_NAME.pid"
23 LOG_PATH="$APP_PATH/$APP_NAME.log"
23 LOG_PATH="$APP_PATH/$APP_NAME.log"
24
24
25 PYTHON_PATH="/$APP_HOMEDIR/$APP_NAME-venv"
25 PYTHON_PATH="/$APP_HOMEDIR/$APP_NAME-venv"
26
26
27 RUN_AS="root"
27 RUN_AS="root"
28
28
29 DAEMON="$PYTHON_PATH/bin/paster"
29 DAEMON="$PYTHON_PATH/bin/gearbox"
30
30
31 DAEMON_OPTS="serve --daemon \
31 DAEMON_OPTS="serve --daemon \
32 --user=$RUN_AS \
32 --user=$RUN_AS \
33 --group=$RUN_AS \
33 --group=$RUN_AS \
34 --pid-file=$PID_PATH \
34 --pid-file=$PID_PATH \
35 --log-file=$LOG_PATH $APP_PATH/$CONF_NAME"
35 --log-file=$LOG_PATH -c $APP_PATH/$CONF_NAME"
36
36
37
37
38 start() {
38 start() {
39 echo "Starting $APP_NAME"
39 echo "Starting $APP_NAME"
40 PYTHON_EGG_CACHE="/tmp" start-stop-daemon -d $APP_PATH \
40 PYTHON_EGG_CACHE="/tmp" start-stop-daemon -d $APP_PATH \
@@ -43,18 +43,18 b' start() {'
43 --user $RUN_AS \
43 --user $RUN_AS \
44 --exec $DAEMON -- $DAEMON_OPTS
44 --exec $DAEMON -- $DAEMON_OPTS
45 }
45 }
46
46
47 stop() {
47 stop() {
48 echo "Stopping $APP_NAME"
48 echo "Stopping $APP_NAME"
49 start-stop-daemon -d $APP_PATH \
49 start-stop-daemon -d $APP_PATH \
50 --stop --quiet \
50 --stop --quiet \
51 --pidfile $PID_PATH || echo "$APP_NAME - Not running!"
51 --pidfile $PID_PATH || echo "$APP_NAME - Not running!"
52
52
53 if [ -f $PID_PATH ]; then
53 if [ -f $PID_PATH ]; then
54 rm $PID_PATH
54 rm $PID_PATH
55 fi
55 fi
56 }
56 }
57
57
58 status() {
58 status() {
59 echo -n "Checking status of $APP_NAME ... "
59 echo -n "Checking status of $APP_NAME ... "
60 pid=`cat $PID_PATH`
60 pid=`cat $PID_PATH`
@@ -65,7 +65,7 b' status() {'
65 echo "NOT running"
65 echo "NOT running"
66 fi
66 fi
67 }
67 }
68
68
69 case "$1" in
69 case "$1" in
70 status)
70 status)
71 status
71 status
@@ -87,4 +87,4 b' case "$1" in'
87 *)
87 *)
88 echo "Usage: $0 {start|stop|restart}"
88 echo "Usage: $0 {start|stop|restart}"
89 exit 1
89 exit 1
90 esac No newline at end of file
90 esac
@@ -16,13 +16,13 b' PYTHON_PATH="/home/$APP_HOMEDIR/v-env"'
16
16
17 RUN_AS="username"
17 RUN_AS="username"
18
18
19 DAEMON="$PYTHON_PATH/bin/paster"
19 DAEMON="$PYTHON_PATH/bin/gearbox"
20
20
21 DAEMON_OPTS="serve --daemon \
21 DAEMON_OPTS="serve --daemon \
22 --user=$RUN_AS \
22 --user=$RUN_AS \
23 --group=$RUN_AS \
23 --group=$RUN_AS \
24 --pid-file=$PID_PATH \
24 --pid-file=$PID_PATH \
25 --log-file=$LOG_PATH $APP_PATH/$CONF_NAME"
25 --log-file=$LOG_PATH -c $APP_PATH/$CONF_NAME"
26
26
27 #extra options
27 #extra options
28 opts="${opts} restartdelay"
28 opts="${opts} restartdelay"
@@ -56,6 +56,6 b' restartdelay() {'
56 #stop()
56 #stop()
57 echo "sleep3"
57 echo "sleep3"
58 sleep 3
58 sleep 3
59
59
60 #start()
60 #start()
61 }
61 }
@@ -20,7 +20,7 b' APP_PATH="/var/www/$APP_NAME"'
20 CONF_NAME="production.ini"
20 CONF_NAME="production.ini"
21
21
22 # write to wherever the PID should be stored, just ensure
22 # write to wherever the PID should be stored, just ensure
23 # that the user you run paster as has the appropriate permissions
23 # that the user you run gearbox as has the appropriate permissions
24 # same goes for the log file
24 # same goes for the log file
25 PID_PATH="/var/run/kallithea/pid"
25 PID_PATH="/var/run/kallithea/pid"
26 LOG_PATH="/var/log/kallithea/kallithea.log"
26 LOG_PATH="/var/log/kallithea/kallithea.log"
@@ -31,13 +31,13 b' PYTHON_PATH="/opt/python_virtualenvironm'
31
31
32 RUN_AS="kallithea"
32 RUN_AS="kallithea"
33
33
34 DAEMON="$PYTHON_PATH/bin/paster"
34 DAEMON="$PYTHON_PATH/bin/gearbox"
35
35
36 DAEMON_OPTS="serve --daemon \
36 DAEMON_OPTS="serve --daemon \
37 --user=$RUN_AS \
37 --user=$RUN_AS \
38 --group=$RUN_AS \
38 --group=$RUN_AS \
39 --pid-file=$PID_PATH \
39 --pid-file=$PID_PATH \
40 --log-file=$LOG_PATH $APP_PATH/$CONF_NAME"
40 --log-file=$LOG_PATH -c $APP_PATH/$CONF_NAME"
41
41
42 DESC="kallithea-server"
42 DESC="kallithea-server"
43 LOCK_FILE="/var/lock/subsys/$APP_NAME"
43 LOCK_FILE="/var/lock/subsys/$APP_NAME"
@@ -129,4 +129,4 b' case "$1" in'
129 ;;
129 ;;
130 esac
130 esac
131
131
132 exit $RETVAL No newline at end of file
132 exit $RETVAL
@@ -2,8 +2,8 b''
2 # Change variables/paths as necessary and place file /etc/init/kallithea.conf
2 # Change variables/paths as necessary and place file /etc/init/kallithea.conf
3 # start/stop/restart as normal upstart job (ie: $ start kallithea)
3 # start/stop/restart as normal upstart job (ie: $ start kallithea)
4
4
5 description "Kallithea Mercurial Server"
5 description "Kallithea Mercurial Server"
6 author "Matt Zuba <matt.zuba@goodwillaz.org"
6 author "Matt Zuba <matt.zuba@goodwillaz.org"
7
7
8 start on (local-filesystems and runlevel [2345])
8 start on (local-filesystems and runlevel [2345])
9 stop on runlevel [!2345]
9 stop on runlevel [!2345]
@@ -19,8 +19,8 b' env HOME=/var/hg'
19 env USER=hg
19 env USER=hg
20 env GROUP=hg
20 env GROUP=hg
21
21
22 exec /var/hg/.virtualenvs/kallithea/bin/paster serve --user=$USER --group=$GROUP --pid-file=$PIDFILE --log-file=$LOGFILE $APPINI
22 exec /var/hg/.virtualenvs/kallithea/bin/gearbox serve --user=$USER --group=$GROUP --pid-file=$PIDFILE --log-file=$LOGFILE -c $APPINI
23
23
24 post-stop script
24 post-stop script
25 rm -f $PIDFILE
25 rm -f $PIDFILE
26 end script
26 end script
@@ -45,7 +45,7 b' serverurl=http://127.0.0.1:9001 ; use an'
45 numprocs = 1
45 numprocs = 1
46 numprocs_start = 5000 # possible should match ports
46 numprocs_start = 5000 # possible should match ports
47 directory=/srv/kallithea
47 directory=/srv/kallithea
48 command = /srv/kallithea/venv/bin/paster serve my.ini
48 command = /srv/kallithea/venv/bin/gearbox serve -c my.ini
49 process_name = %(program_name)s_%(process_num)04d
49 process_name = %(program_name)s_%(process_num)04d
50 redirect_stderr=true
50 redirect_stderr=true
51 stdout_logfile=/%(here)s/kallithea.log
51 stdout_logfile=/%(here)s/kallithea.log
@@ -15,8 +15,9 b''
15 kallithea
15 kallithea
16 ~~~~~~~~~
16 ~~~~~~~~~
17
17
18 Kallithea, a web based repository management based on pylons
18 Kallithea, a web based repository management system.
19 versioning implementation: http://www.python.org/dev/peps/pep-0386/
19
20 Versioning implementation: http://www.python.org/dev/peps/pep-0386/
20
21
21 This file was forked by the Kallithea project in July 2014.
22 This file was forked by the Kallithea project in July 2014.
22 Original author and date, and relevant copyright and licensing information is below:
23 Original author and date, and relevant copyright and licensing information is below:
@@ -29,7 +30,7 b' Original author and date, and relevant c'
29 import sys
30 import sys
30 import platform
31 import platform
31
32
32 VERSION = (0, 3, 7)
33 VERSION = (0, 4, 0, 'rc2')
33 BACKENDS = {
34 BACKENDS = {
34 'hg': 'Mercurial repository',
35 'hg': 'Mercurial repository',
35 'git': 'Git repository',
36 'git': 'Git repository',
@@ -38,45 +39,20 b' BACKENDS = {'
38 CELERY_ON = False
39 CELERY_ON = False
39 CELERY_EAGER = False
40 CELERY_EAGER = False
40
41
41 # link to config for pylons
42 CONFIG = {}
42 CONFIG = {}
43
43
44 # Linked module for extensions
44 # Linked module for extensions
45 EXTENSIONS = {}
45 EXTENSIONS = {}
46
46
47 # BRAND controls internal references in database and config to the products
48 # own name.
49 #
50 # NOTE: If you want compatibility with a database that was originally created
51 # for use with the RhodeCode software product, change BRAND to "rhodecode",
52 # either by editing here or by creating a new file:
53 # echo "BRAND = 'rhodecode'" > kallithea/brand.py
54
55 BRAND = "kallithea"
56 try:
47 try:
57 from kallithea.brand import BRAND
48 import kallithea.brand
58 except ImportError:
49 except ImportError:
59 pass
50 pass
60
51 else:
61 # Prefix for the ui and settings table names
52 assert False, 'Database rebranding is no longer supported; see README.'
62 DB_PREFIX = (BRAND + "_") if BRAND != "kallithea" else ""
63
64 # Users.extern_type and .extern_name value for local users
65 EXTERN_TYPE_INTERNAL = BRAND if BRAND != 'kallithea' else 'internal'
66
67 # db_migrate_version.repository_id value, same as kallithea/lib/dbmigrate/migrate.cfg
68 DB_MIGRATIONS = BRAND + "_db_migrations"
69
53
70 try:
71 from kallithea.lib import get_current_revision
72 _rev = get_current_revision(quiet=True)
73 if _rev and len(VERSION) > 3:
74 VERSION += (_rev[0],)
75 except ImportError:
76 pass
77
54
78 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
55 __version__ = '.'.join(str(each) for each in VERSION)
79 __dbversion__ = 31 # defines current db version for migrations
80 __platform__ = platform.system()
56 __platform__ = platform.system()
81 __license__ = 'GPLv3'
57 __license__ = 'GPLv3'
82 __py_version__ = sys.version_info
58 __py_version__ = sys.version_info
@@ -85,17 +61,3 b' except ImportError:'
85
61
86 is_windows = __platform__ in ['Windows']
62 is_windows = __platform__ in ['Windows']
87 is_unix = not is_windows
63 is_unix = not is_windows
88
89 if len(VERSION) > 3:
90 __version__ += '.'+VERSION[3]
91
92 if len(VERSION) > 4:
93 __version__ += VERSION[4]
94 else:
95 __version__ += '0'
96
97 # Hack for making the celery dependency kombu==1.5.1 compatible with Python
98 # 2.7.11 which has https://hg.python.org/releases/2.7.11/rev/24bdc4940e81
99 import uuid
100 if not hasattr(uuid, '_uuid_generate_random'):
101 uuid._uuid_generate_random = None
@@ -111,8 +111,8 b' class RcConf(object):'
111 return True
111 return True
112 return False
112 return False
113
113
114 def __eq__(self):
114 def __eq__(self, other):
115 return self._conf.__eq__()
115 return self._conf.__eq__(other)
116
116
117 def __repr__(self):
117 def __repr__(self):
118 return 'RcConf<%s>' % self._conf.__repr__()
118 return 'RcConf<%s>' % self._conf.__repr__()
@@ -158,7 +158,7 b' class RcConf(object):'
158 """
158 """
159 try:
159 try:
160 with open(self._conf_name, 'rb') as conf:
160 with open(self._conf_name, 'rb') as conf:
161 return json.load(conf)
161 return json.load(conf)
162 except IOError as e:
162 except IOError as e:
163 #sys.stderr.write(str(e) + '\n')
163 #sys.stderr.write(str(e) + '\n')
164 pass
164 pass
@@ -121,5 +121,6 b' def main(argv=None):'
121 )
121 )
122 return 0
122 return 0
123
123
124
124 if __name__ == '__main__':
125 if __name__ == '__main__':
125 sys.exit(main(sys.argv))
126 sys.exit(main(sys.argv))
@@ -28,11 +28,11 b' Original author and date, and relevant c'
28
28
29 import os
29 import os
30 import sys
30 import sys
31
32 import logging
31 import logging
33 import tarfile
32 import tarfile
34 import datetime
33 import datetime
35 import subprocess
34 import subprocess
35 import tempfile
36
36
37 logging.basicConfig(level=logging.DEBUG,
37 logging.basicConfig(level=logging.DEBUG,
38 format="%(asctime)s %(levelname)-5.5s %(message)s")
38 format="%(asctime)s %(levelname)-5.5s %(message)s")
@@ -47,7 +47,7 b' class BackupManager(object):'
47 self.repos_path = self.get_repos_path(repos_location)
47 self.repos_path = self.get_repos_path(repos_location)
48 self.backup_server = backup_server
48 self.backup_server = backup_server
49
49
50 self.backup_file_path = '/tmp'
50 self.backup_file_path = tempfile.gettempdir()
51
51
52 logging.info('starting backup for %s', self.repos_path)
52 logging.info('starting backup for %s', self.repos_path)
53 logging.info('backup target %s', self.backup_file_path)
53 logging.info('backup target %s', self.backup_file_path)
@@ -86,12 +86,13 b' class BackupManager(object):'
86 '%(backup_server)s' % params]
86 '%(backup_server)s' % params]
87
87
88 subprocess.call(cmd)
88 subprocess.call(cmd)
89 logging.info('Transfered file %s to %s', self.backup_file_name, cmd[4])
89 logging.info('Transferred file %s to %s', self.backup_file_name, cmd[4])
90
90
91 def rm_file(self):
91 def rm_file(self):
92 logging.info('Removing file %s', self.backup_file_name)
92 logging.info('Removing file %s', self.backup_file_name)
93 os.remove(os.path.join(self.backup_file_path, self.backup_file_name))
93 os.remove(os.path.join(self.backup_file_path, self.backup_file_name))
94
94
95
95 if __name__ == "__main__":
96 if __name__ == "__main__":
96
97
97 repo_location = '/home/repo_path'
98 repo_location = '/home/repo_path'
@@ -1,100 +1,39 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15 import click
16 import kallithea.bin.kallithea_cli_base as cli_base
2
17
3 import kallithea
18 import kallithea
4 from kallithea.lib.utils import BasePasterCommand, Command, load_rcextensions
5 from celery.app import app_or_default
6 from celery.bin import camqadm, celerybeat, celeryd, celeryev
7
19
8 from kallithea.lib.utils2 import str2bool
20 @cli_base.register_command(config_file_initialize_app=True)
21 @click.argument('celery_args', nargs=-1)
22 def celery_run(celery_args):
23 """Start Celery worker(s) for asynchronous tasks.
9
24
10 __all__ = ['CeleryDaemonCommand', 'CeleryBeatCommand',
25 This commands starts the Celery daemon which will spawn workers to handle
11 'CAMQPAdminCommand', 'CeleryEventCommand']
26 certain asynchronous tasks for Kallithea.
12
13
27
14 class CeleryCommand(BasePasterCommand):
28 Any extra arguments you pass to this command will be passed through to
15 """Abstract class implements run methods needed for celery
29 Celery. Use '--' before such extra arguments to avoid options to be parsed
16
30 by this CLI command.
17 Starts the celery worker that uses a paste.deploy configuration
18 file.
19 """
31 """
20
32
21 def update_parser(self):
33 if not kallithea.CELERY_ON:
22 """
34 raise Exception('Please set use_celery = true in .ini config '
23 Abstract method. Allows for the class's parser to be updated
35 'file before running this command')
24 before the superclass's `run` method is called. Necessary to
25 allow options/arguments to be passed through to the underlying
26 celery command.
27 """
28
29 cmd = self.celery_command(app_or_default())
30 for x in cmd.get_options():
31 self.parser.add_option(x)
32
33 def command(self):
34 from pylons import config
35 try:
36 CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
37 except KeyError:
38 CELERY_ON = False
39
40 if not CELERY_ON:
41 raise Exception('Please set use_celery = true in .ini config '
42 'file before running celeryd')
43 kallithea.CELERY_ON = CELERY_ON
44 load_rcextensions(config['here'])
45 cmd = self.celery_command(app_or_default())
46 return cmd.run(**vars(self.options))
47
48
49 class CeleryDaemonCommand(CeleryCommand):
50 """Start the celery worker
51
52 Starts the celery worker that uses a paste.deploy configuration
53 file.
54 """
55 usage = 'CONFIG_FILE [celeryd options...]'
56 summary = __doc__.splitlines()[0]
57 description = "".join(__doc__.splitlines()[2:])
58
36
59 parser = Command.standard_parser(quiet=True)
37 from kallithea.lib import celerypylons
60 celery_command = celeryd.WorkerCommand
38 cmd = celerypylons.worker.worker(celerypylons.app)
61
39 return cmd.run_from_argv(None, command='celery-run -c CONFIG_FILE --', argv=list(celery_args))
62
63 class CeleryBeatCommand(CeleryCommand):
64 """Start the celery beat server
65
66 Starts the celery beat server using a paste.deploy configuration
67 file.
68 """
69 usage = 'CONFIG_FILE [celerybeat options...]'
70 summary = __doc__.splitlines()[0]
71 description = "".join(__doc__.splitlines()[2:])
72
73 parser = Command.standard_parser(quiet=True)
74 celery_command = celerybeat.BeatCommand
75
76
77 class CAMQPAdminCommand(CeleryCommand):
78 """CAMQP Admin
79
80 CAMQP celery admin tool.
81 """
82 usage = 'CONFIG_FILE [camqadm options...]'
83 summary = __doc__.splitlines()[0]
84 description = "".join(__doc__.splitlines()[2:])
85
86 parser = Command.standard_parser(quiet=True)
87 celery_command = camqadm.AMQPAdminCommand
88
89
90 class CeleryEventCommand(CeleryCommand):
91 """Celery event command.
92
93 Capture celery events.
94 """
95 usage = 'CONFIG_FILE [celeryev options...]'
96 summary = __doc__.splitlines()[0]
97 description = "".join(__doc__.splitlines()[2:])
98
99 parser = Command.standard_parser(quiet=True)
100 celery_command = celeryev.EvCommand
@@ -1,5 +1,3 b''
1 #!/usr/bin/env python2
2
3 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
4 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
@@ -13,148 +11,82 b''
13 #
11 #
14 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 """
17 kallithea.bin.kallithea_config
18 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
19
14
20 configuration generator for Kallithea
15 import click
21
16 import kallithea.bin.kallithea_cli_base as cli_base
22 This file was forked by the Kallithea project in July 2014.
23 Original author and date, and relevant copyright and licensing information is below:
24 :created_on: Jun 18, 2013
25 :author: marcink
26 :copyright: (c) 2013 RhodeCode GmbH, and others.
27 :license: GPLv3, see LICENSE.md for more details.
28 """
29
30
17
31 import os
18 import os
32 import sys
33 import uuid
19 import uuid
34 import argparse
20 from collections import defaultdict
35 from mako.template import Template
21
36 TMPL = 'template.ini.mako'
22 import mako.exceptions
37 here = os.path.dirname(os.path.abspath(__file__))
23
24 from kallithea.lib import inifile
38
25
39 def argparser(argv):
26 def show_defaults(ctx, param, value):
40 usage = (
27 # Following construct is taken from the Click documentation:
41 "kallithea-config [-h] [--filename=FILENAME] [--template=TEMPLATE] \n"
28 # https://click.palletsprojects.com/en/7.x/options/#callbacks-and-eager-options
42 "VARS optional specify extra template variable that will be available in "
29 # "The resilient_parsing flag is applied to the context if Click wants to
43 "template. Use comma separated key=val format eg.\n"
30 # parse the command line without any destructive behavior that would change
44 "key1=val1,port=5000,host=127.0.0.1,elements='a\,b\,c'\n"
31 # the execution flow. In this case, because we would exit the program, we
45 )
32 # instead do nothing."
33 if not value or ctx.resilient_parsing:
34 return
46
35
47 parser = argparse.ArgumentParser(
36 for key, value in inifile.default_variables.items():
48 description='Kallithea CONFIG generator with variable replacement',
37 click.echo('%s=%s' % (key, value))
49 usage=usage
50 )
51
38
52 ## config
39 ctx.exit()
53 group = parser.add_argument_group('CONFIG')
40
54 group.add_argument('--filename', help='Output ini filename.')
41 @cli_base.register_command()
55 group.add_argument('--template', help='Mako template file to use instead of '
42 @click.option('--show-defaults', callback=show_defaults,
56 'the default builtin template')
43 is_flag=True, expose_value=False, is_eager=True,
57 group.add_argument('--raw', help='Store given mako template as raw without '
44 help='Show the default values that can be overridden')
58 'parsing. Use this to create custom template '
45 @click.argument('config_file', type=click.Path(dir_okay=False, writable=True), required=True)
59 'initially', action='store_true')
46 @click.argument('key_value_pairs', nargs=-1)
60 group.add_argument('--show-defaults', help='Show all default variables for '
47 def config_create(config_file, key_value_pairs):
61 'builtin template', action='store_true')
48 """Create a new configuration file.
62 args, other = parser.parse_known_args()
63 return parser, args, other
64
49
50 This command creates a default configuration file, possibly adding/updating
51 settings you specify.
65
52
66 def _escape_split(text, sep):
53 The primary high level configuration keys and their default values are
67 """
54 shown with --show-defaults . Custom values for these keys can be specified
68 Allows for escaping of the separator: e.g. arg='foo\, bar'
55 on the command line as key=value arguments.
69
56
70 It should be noted that the way bash et. al. do command line parsing, those
57 Additional key=value arguments will be patched/inserted in the [app:main]
71 single quotes are required. a shameless ripoff from fabric project.
58 section ... until another section name specifies where any following values
72
59 should go.
73 """
60 """
74 escaped_sep = r'\%s' % sep
75
61
76 if escaped_sep not in text:
62 mako_variable_values = {}
77 return text.split(sep)
63 ini_settings = defaultdict(dict)
78
79 before, _, after = text.partition(escaped_sep)
80 startlist = before.split(sep) # a regular split is fine here
81 unfinished = startlist[-1]
82 startlist = startlist[:-1]
83
84 # recurse because there may be more escaped separators
85 endlist = _escape_split(after, sep)
86
87 # finish building the escaped value. we use endlist[0] becaue the first
88 # part of the string sent in recursion is the rest of the escaped value.
89 unfinished += sep + endlist[0]
90
91 return startlist + [unfinished] + endlist[1:] # put together all the parts
92
64
93 def _run(argv):
65 section_name = None
94 parser, args, other = argparser(argv)
66 for parameter in key_value_pairs:
95 if not len(sys.argv) > 1:
67 parts = parameter.split('=', 1)
96 print parser.print_help()
68 if len(parts) == 1 and parameter.startswith('[') and parameter.endswith(']'):
97 sys.exit(0)
69 section_name = parameter
98 # defaults that can be overwritten by arguments
70 elif len(parts) == 2:
99 tmpl_stored_args = {
71 key, value = parts
100 'http_server': 'waitress',
72 if section_name is None and key in inifile.default_variables:
101 'lang': 'en',
73 mako_variable_values[key] = value
102 'database_engine': 'sqlite',
74 else:
103 'host': '127.0.0.1',
75 if section_name is None:
104 'port': 5000,
76 section_name = '[app:main]'
105 'error_aggregation_service': None,
77 ini_settings[section_name][key] = value
106 }
78 else:
107 if other:
79 raise ValueError("Invalid name=value parameter %r" % parameter)
108 # parse arguments, we assume only first is correct
109 kwargs = {}
110 for el in _escape_split(other[0], ','):
111 kv = _escape_split(el, '=')
112 if len(kv) == 2:
113 k, v = kv
114 kwargs[k] = v
115 # update our template stored args
116 tmpl_stored_args.update(kwargs)
117
80
118 # use default that cannot be replaced
81 # use default that cannot be replaced
119 tmpl_stored_args.update({
82 mako_variable_values.update({
120 'uuid': lambda: uuid.uuid4().hex,
83 'uuid': lambda: uuid.uuid4().hex,
121 'here': os.path.abspath(os.curdir),
122 })
84 })
123 if args.show_defaults:
124 for k,v in tmpl_stored_args.iteritems():
125 print '%s=%s' % (k, v)
126 sys.exit(0)
127 try:
85 try:
128 # built in template
86 config_file_abs = os.path.abspath(config_file)
129 tmpl_file = os.path.join(here, TMPL)
87 inifile.create(config_file_abs, mako_variable_values, ini_settings)
130 if args.template:
88 click.echo('Wrote new config file in %s' % config_file_abs)
131 tmpl_file = args.template
89 click.echo("Don't forget to build the front-end using 'kallithea-cli front-end-build'.")
132
133 with open(tmpl_file, 'rb') as f:
134 tmpl_data = f.read().decode('utf-8')
135 if args.raw:
136 tmpl = tmpl_data
137 else:
138 tmpl = Template(tmpl_data).render(**tmpl_stored_args)
139 with open(args.filename, 'wb') as f:
140 f.write(tmpl.encode('utf-8'))
141 print 'Wrote new config file in %s' % (os.path.abspath(args.filename))
142
90
143 except Exception:
91 except Exception:
144 from mako import exceptions
92 click.echo(mako.exceptions.text_error_template().render())
145 print exceptions.text_error_template().render()
146
147 def main(argv=None):
148 """
149 Main execution function for cli
150
151 :param argv:
152 """
153 if argv is None:
154 argv = sys.argv
155
156 return _run(argv)
157
158
159 if __name__ == '__main__':
160 sys.exit(main(sys.argv))
@@ -1,112 +1,79 b''
1 import os
1 # -*- coding: utf-8 -*-
2 import sys
2 # This program is free software: you can redistribute it and/or modify
3 from paste.script.appinstall import AbstractInstallCommand
3 # it under the terms of the GNU General Public License as published by
4 from paste.script.command import BadCommand
4 # the Free Software Foundation, either version 3 of the License, or
5 from paste.deploy import appconfig
5 # (at your option) any later version.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 import click
15 import kallithea.bin.kallithea_cli_base as cli_base
6
16
7 # Add location of top level folder to sys.path
17 import kallithea
8 from os.path import dirname as dn
18 from kallithea.lib.db_manage import DbManage
9 rc_path = dn(dn(dn(os.path.realpath(__file__))))
19 from kallithea.model.meta import Session
10 sys.path.append(rc_path)
11
12
13 class Command(AbstractInstallCommand):
14
20
15 default_verbosity = 1
21 @cli_base.register_command(config_file=True)
16 max_args = 1
22 @click.option('--user', help='Username of administrator account.')
17 min_args = 1
23 @click.option('--password', help='Password for administrator account.')
18 summary = "Setup an application, given a config file"
24 @click.option('--email', help='Email address of administrator account.')
19 usage = "CONFIG_FILE"
25 @click.option('--repos', help='Absolute path to repositories location.')
20 group_name = "Kallithea"
26 @click.option('--force-yes', is_flag=True, help='Answer yes to every question.')
27 @click.option('--force-no', is_flag=True, help='Answer no to every question.')
28 @click.option('--public-access/--no-public-access', default=True,
29 help='Enable/disable public access on this installation (default: enable)')
30 def db_create(user, password, email, repos, force_yes, force_no, public_access):
31 """Initialize the database.
21
32
22 description = """\
33 Create all required tables in the database specified in the configuration
34 file. Create the administrator account. Set certain settings based on
35 values you provide.
23
36
24 Setup Kallithea according to its configuration file. This is
37 You can pass the answers to all questions as options to this command.
25 the second part of a two-phase web application installation
26 process (the first phase is prepare-app). The setup process
27 consist of things like setting up databases, creating super user
28 """
38 """
39 dbconf = kallithea.CONFIG['sqlalchemy.url']
29
40
30 parser = AbstractInstallCommand.standard_parser(
41 # force_ask should be True (yes), False (no), or None (ask)
31 simulate=True, quiet=True, interactive=True)
42 if force_yes:
32 parser.add_option('--user',
43 force_ask = True
33 action='store',
44 elif force_no:
34 dest='username',
45 force_ask = False
35 default=None,
46 else:
36 help='Admin Username')
47 force_ask = None
37 parser.add_option('--email',
38 action='store',
39 dest='email',
40 default=None,
41 help='Admin Email')
42 parser.add_option('--password',
43 action='store',
44 dest='password',
45 default=None,
46 help='Admin password min 6 chars')
47 parser.add_option('--repos',
48 action='store',
49 dest='repos_location',
50 default=None,
51 help='Absolute path to repositories location')
52 parser.add_option('--name',
53 action='store',
54 dest='section_name',
55 default=None,
56 help='The name of the section to set up (default: app:main)')
57 parser.add_option('--force-yes',
58 action='store_true',
59 dest='force_ask',
60 default=None,
61 help='Force yes to every question')
62 parser.add_option('--force-no',
63 action='store_false',
64 dest='force_ask',
65 default=None,
66 help='Force no to every question')
67 parser.add_option('--public-access',
68 action='store_true',
69 dest='public_access',
70 default=None,
71 help='Enable public access on this installation (default)')
72 parser.add_option('--no-public-access',
73 action='store_false',
74 dest='public_access',
75 default=None,
76 help='Disable public access on this installation ')
77 def command(self):
78 config_spec = self.args[0]
79 section = self.options.section_name
80 if section is None:
81 if '#' in config_spec:
82 config_spec, section = config_spec.split('#', 1)
83 else:
84 section = 'main'
85 if not ':' in section:
86 plain_section = section
87 section = 'app:' + section
88 else:
89 plain_section = section.split(':', 1)[0]
90 if not config_spec.startswith('config:'):
91 config_spec = 'config:' + config_spec
92 if plain_section != 'main':
93 config_spec += '#' + plain_section
94 config_file = config_spec[len('config:'):].split('#', 1)[0]
95 config_file = os.path.join(os.getcwd(), config_file)
96 self.logging_file_config(config_file)
97 conf = appconfig(config_spec, relative_to=os.getcwd())
98 ep_name = conf.context.entry_point_name
99 ep_group = conf.context.protocol
100 dist = conf.context.distribution
101 if dist is None:
102 raise BadCommand(
103 "The section %r is not the application (probably a filter). "
104 "You should add #section_name, where section_name is the "
105 "section that configures your application" % plain_section)
106 installer = self.get_installer(dist, ep_group, ep_name)
107 installer.setup_config(
108 self, config_file, section, self.sysconfig_install_vars(installer))
109 self.call_sysconfig_functions(
110 'post_setup_hook', installer, config_file)
111
48
112 print 'Database set up successfully.'
49 cli_args = dict(
50 username=user,
51 password=password,
52 email=email,
53 repos_location=repos,
54 force_ask=force_ask,
55 public_access=public_access,
56 )
57 dbmanage = DbManage(dbconf=dbconf, root=kallithea.CONFIG['here'],
58 tests=False, cli_args=cli_args)
59 dbmanage.create_tables(override=True)
60 opts = dbmanage.config_prompt(None)
61 dbmanage.create_settings(opts)
62 dbmanage.create_default_user()
63 dbmanage.admin_prompt()
64 dbmanage.create_permissions()
65 dbmanage.populate_default_permissions()
66 Session().commit()
67
68 # initial repository scan
69 kallithea.config.middleware.make_app_without_logging(
70 kallithea.CONFIG.global_conf, **kallithea.CONFIG.local_conf)
71 added, _ = kallithea.lib.utils.repo2db_mapper(kallithea.model.scm.ScmModel().repo_scan())
72 if added:
73 click.echo('Initial repository scan: added following repositories:')
74 click.echo('\t%s' % '\n\t'.join(added))
75 else:
76 click.echo('Initial repository scan: no repositories found.')
77
78 click.echo('Database set up successfully.')
79 click.echo("Don't forget to build the front-end using 'kallithea-cli front-end-build'.")
@@ -12,70 +12,45 b''
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.paster_commands.make_rcextensions
15 This file was forked by the Kallithea project in July 2014 and later moved.
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
18 make-rcext paster command for Kallithea
19
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
16 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Mar 6, 2012
17 :created_on: Mar 6, 2012
23 :author: marcink
18 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
19 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
20 :license: GPLv3, see LICENSE.md for more details.
26
27 """
21 """
28
22 import click
23 import kallithea.bin.kallithea_cli_base as cli_base
29
24
30 import os
25 import os
31 import sys
32 import pkg_resources
26 import pkg_resources
33
27
34 from kallithea.lib.utils import BasePasterCommand, ask_ok
28 import kallithea
35
29 from kallithea.lib.utils2 import ask_ok
36 # Add location of top level folder to sys.path
37 from os.path import dirname as dn
38 rc_path = dn(dn(dn(os.path.realpath(__file__))))
39 sys.path.append(rc_path)
40
41
42 class Command(BasePasterCommand):
43
30
44 max_args = 1
31 @cli_base.register_command(config_file=True)
45 min_args = 1
32 def extensions_create():
46
33 """Write template file for extending Kallithea in Python.
47 group_name = "Kallithea"
48 takes_config_file = -1
49 parser = BasePasterCommand.standard_parser(verbose=True)
50 summary = "Write template file for extending Kallithea in Python."
51 usage = "CONFIG_FILE"
52 description = '''\
53 A rcextensions directory with a __init__.py file will be created next to
54 the ini file. Local customizations in that file will survive upgrades.
55 The file contains instructions on how it can be customized.
56 '''
57
34
58 def command(self):
35 An rcextensions directory with a __init__.py file will be created next to
59 from pylons import config
36 the ini file. Local customizations in that file will survive upgrades.
37 The file contains instructions on how it can be customized.
38 """
39 here = kallithea.CONFIG['here']
40 content = pkg_resources.resource_string(
41 'kallithea', os.path.join('config', 'rcextensions', '__init__.py')
42 )
43 ext_file = os.path.join(here, 'rcextensions', '__init__.py')
44 if os.path.exists(ext_file):
45 msg = ('Extension file %s already exists, do you want '
46 'to overwrite it ? [y/n] ') % ext_file
47 if not ask_ok(msg):
48 click.echo('Nothing done, exiting...')
49 return
60
50
61 here = config['here']
51 dirname = os.path.dirname(ext_file)
62 content = pkg_resources.resource_string(
52 if not os.path.isdir(dirname):
63 'kallithea', os.path.join('config', 'rcextensions', '__init__.py')
53 os.makedirs(dirname)
64 )
54 with open(ext_file, 'wb') as f:
65 ext_file = os.path.join(here, 'rcextensions', '__init__.py')
55 f.write(content)
66 if os.path.exists(ext_file):
56 click.echo('Wrote new extensions file to %s' % ext_file)
67 msg = ('Extension file already exists, do you want '
68 'to overwrite it ? [y/n]')
69 if not ask_ok(msg):
70 print 'Nothing done...'
71 return
72
73 dirname = os.path.dirname(ext_file)
74 if not os.path.isdir(dirname):
75 os.makedirs(dirname)
76 with open(ext_file, 'wb') as f:
77 f.write(content)
78 print 'Wrote new extensions file to %s' % ext_file
79
80 def update_parser(self):
81 pass
@@ -1,43 +1,26 b''
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 import click
15 import kallithea
16 import kallithea.bin.kallithea_cli_base as cli_base
17
1 import os
18 import os
2 import sys
19 import sys
3 from paste.script.appinstall import AbstractInstallCommand
4 from paste.script.command import BadCommand
5
20
6 # Add location of top level folder to sys.path
21 dispath_py_template = '''\
7 from os.path import dirname as dn
22 # Created by Kallithea 'kallithea-cli iis-install'
8 rc_path = dn(dn(dn(os.path.realpath(__file__))))
23 import sys
9 sys.path.append(rc_path)
10
11 class Command(AbstractInstallCommand):
12 default_verbosity = 1
13 max_args = 1
14 min_args = 1
15 summary = 'Setup IIS given a config file'
16 usage = 'CONFIG_FILE'
17
18 description = '''
19 Script for installing into IIS using isapi-wsgi.
20 '''
21 parser = AbstractInstallCommand.standard_parser(
22 simulate=True, quiet=True, interactive=True)
23 parser.add_option('--virtualdir',
24 action='store',
25 dest='virtualdir',
26 default='/',
27 help='The virtual folder to install into on IIS')
28
29 def command(self):
30 config_spec = self.args[0]
31 if not config_spec.startswith('config:'):
32 config_spec = 'config:' + config_spec
33 config_file = config_spec[len('config:'):].split('#', 1)[0]
34 config_file = os.path.join(os.getcwd(), config_file)
35 try:
36 import isapi_wsgi
37 except ImportError:
38 raise BadCommand('missing requirement: isapi-wsgi not installed')
39
40 file = '''import sys
41
24
42 if hasattr(sys, "isapidllhandle"):
25 if hasattr(sys, "isapidllhandle"):
43 import win32traceutil
26 import win32traceutil
@@ -47,7 +30,7 b' import os'
47
30
48 def __ExtensionFactory__():
31 def __ExtensionFactory__():
49 from paste.deploy import loadapp
32 from paste.deploy import loadapp
50 from paste.script.util.logging_config import fileConfig
33 from logging.config import fileConfig
51 fileConfig('%(inifile)s')
34 fileConfig('%(inifile)s')
52 application = loadapp('config:%(inifile)s')
35 application = loadapp('config:%(inifile)s')
53
36
@@ -71,15 +54,28 b" if __name__=='__main__':"
71 HandleCommandLine(params)
54 HandleCommandLine(params)
72 '''
55 '''
73
56
74 outdata = file % {
57 @cli_base.register_command(config_file=True)
75 'inifile': config_file.replace('\\', '\\\\'),
58 @click.option('--virtualdir', default='/',
76 'virtualdir': self.options.virtualdir
59 help='The virtual folder to install into on IIS.')
77 }
60 def iis_install(virtualdir):
61 """Install into IIS using isapi-wsgi."""
62
63 config_file_abs = kallithea.CONFIG['__file__']
78
64
79 dispatchfile = os.path.join(os.getcwd(), 'dispatch.py')
65 try:
80 self.ensure_file(dispatchfile, outdata, False)
66 import isapi_wsgi
81 print 'generating', dispatchfile
67 except ImportError:
68 sys.stderr.write('missing requirement: isapi-wsgi not installed\n')
69 sys.exit(1)
82
70
83 print ('run \'python "%s" install\' with administrative privileges '
71 dispatchfile = os.path.join(os.getcwd(), 'dispatch.py')
84 'to generate the _dispatch.dll file and install it into the '
72 click.echo('Writing %s' % dispatchfile)
85 'default web site') % (dispatchfile,)
73 with open(dispatchfile, 'w') as f:
74 f.write(dispath_py_template % {
75 'inifile': config_file_abs.replace('\\', '\\\\'),
76 'virtualdir': virtualdir,
77 })
78
79 click.echo('Run \'python "%s" install\' with administrative privileges '
80 'to generate the _dispatch.dll file and install it into the '
81 'default web site' % dispatchfile)
@@ -12,101 +12,51 b''
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.paster_commands.make_index
15 This file was forked by the Kallithea project in July 2014 and later moved.
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
18 make-index paster command for Kallithea
19
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
16 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Aug 17, 2010
17 :created_on: Aug 17, 2010
23 :author: marcink
18 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
19 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
20 :license: GPLv3, see LICENSE.md for more details.
26
27 """
21 """
28
22 import click
23 import kallithea.bin.kallithea_cli_base as cli_base
29
24
30 import os
25 import os
26 from string import strip
31 import sys
27 import sys
32 import logging
33
34 from string import strip
35 from kallithea.model.repo import RepoModel
36 from kallithea.lib.utils import BasePasterCommand, load_rcextensions
37
38 # Add location of top level folder to sys.path
39 from os.path import dirname as dn
40 rc_path = dn(dn(dn(os.path.realpath(__file__))))
41 sys.path.append(rc_path)
42
43
44 class Command(BasePasterCommand):
45
46 max_args = 1
47 min_args = 1
48
28
49 usage = "CONFIG_FILE"
29 import kallithea
50 group_name = "Kallithea"
30 from kallithea.lib.indexers.daemon import WhooshIndexingDaemon
51 takes_config_file = -1
31 from kallithea.lib.pidlock import LockHeld, DaemonLock
52 parser = BasePasterCommand.standard_parser(verbose=True)
32 from kallithea.lib.utils import load_rcextensions
53 summary = "Creates or updates full text search index"
33 from kallithea.model.repo import RepoModel
54
34
55 def command(self):
35 @cli_base.register_command(config_file_initialize_app=True)
56 logging.config.fileConfig(self.path_to_ini_file)
36 @click.option('--repo-location', help='Base path of repositories to index. Default: all')
57 #get SqlAlchemy session
37 @click.option('--index-only', help='Comma-separated list of repositories to build index on. Default: all')
58 self._init_session()
38 @click.option('--update-only', help='Comma-separated list of repositories to re-build index on. Default: all')
59 from pylons import config
39 @click.option('-f', '--full', 'full_index', help='Recreate the index from scratch')
60 index_location = config['index_dir']
40 def index_create(repo_location, index_only, update_only, full_index):
61 load_rcextensions(config['here'])
41 """Create or update full text search index"""
62
63 repo_location = self.options.repo_location \
64 if self.options.repo_location else RepoModel().repos_path
65 repo_list = map(strip, self.options.repo_list.split(',')) \
66 if self.options.repo_list else None
67
68 repo_update_list = map(strip, self.options.repo_update_list.split(',')) \
69 if self.options.repo_update_list else None
70
42
71 #======================================================================
43 index_location = kallithea.CONFIG['index_dir']
72 # WHOOSH DAEMON
44 load_rcextensions(kallithea.CONFIG['here'])
73 #======================================================================
45
74 from kallithea.lib.pidlock import LockHeld, DaemonLock
46 if not repo_location:
75 from kallithea.lib.indexers.daemon import WhooshIndexingDaemon
47 repo_location = RepoModel().repos_path
76 try:
48 repo_list = map(strip, index_only.split(',')) \
77 l = DaemonLock(file_=os.path.join(dn(dn(index_location)),
49 if index_only else None
78 'make_index.lock'))
50 repo_update_list = map(strip, update_only.split(',')) \
79 WhooshIndexingDaemon(index_location=index_location,
51 if update_only else None
80 repo_location=repo_location,
81 repo_list=repo_list,
82 repo_update_list=repo_update_list)\
83 .run(full_index=self.options.full_index)
84 l.release()
85 except LockHeld:
86 sys.exit(1)
87
52
88 def update_parser(self):
53 try:
89 self.parser.add_option('--repo-location',
54 l = DaemonLock(os.path.join(index_location, 'make_index.lock'))
90 action='store',
55 WhooshIndexingDaemon(index_location=index_location,
91 dest='repo_location',
56 repo_location=repo_location,
92 help="Specifies repositories location to index OPTIONAL",
57 repo_list=repo_list,
93 )
58 repo_update_list=repo_update_list) \
94 self.parser.add_option('--index-only',
59 .run(full_index=full_index)
95 action='store',
60 l.release()
96 dest='repo_list',
61 except LockHeld:
97 help="Specifies a comma separated list of repositories "
62 sys.exit(1)
98 "to build index on. If not given all repositories "
99 "are scanned for indexing. OPTIONAL",
100 )
101 self.parser.add_option('--update-only',
102 action='store',
103 dest='repo_update_list',
104 help="Specifies a comma separated list of repositories "
105 "to re-build index on. OPTIONAL",
106 )
107 self.parser.add_option('-f',
108 action='store_true',
109 dest='full_index',
110 help="Specifies that index should be made full i.e"
111 " destroy old and build from scratch",
112 default=False)
@@ -12,66 +12,30 b''
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.paster_commands.ishell
15 This file was forked by the Kallithea project in July 2014 and later moved.
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
18 interactive shell paster command for Kallithea
19
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
16 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 4, 2013
17 :created_on: Apr 4, 2013
23 :author: marcink
18 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
19 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
20 :license: GPLv3, see LICENSE.md for more details.
26 """
21 """
27
22 import click
28
23 import kallithea.bin.kallithea_cli_base as cli_base
29 import os
30 import sys
31 import logging
32
33 from kallithea.lib.utils import BasePasterCommand
34
24
35 # Add location of top level folder to sys.path
25 import sys
36 from os.path import dirname as dn
37 rc_path = dn(dn(dn(os.path.realpath(__file__))))
38 sys.path.append(rc_path)
39
26
40 log = logging.getLogger(__name__)
27 # make following imports directly available inside the ishell
41
28 from kallithea.model.db import *
42
43 class Command(BasePasterCommand):
44
45 max_args = 1
46 min_args = 1
47
29
48 usage = "CONFIG_FILE"
30 @cli_base.register_command(config_file_initialize_app=True)
49 group_name = "Kallithea"
31 def ishell():
50 takes_config_file = -1
32 """Interactive shell for Kallithea."""
51 parser = BasePasterCommand.standard_parser(verbose=True)
33 try:
52 summary = "Interactive shell"
34 from IPython import embed
53
35 except ImportError:
54 def command(self):
36 print 'Kallithea ishell requires the Python package IPython 4 or later'
55 #get SqlAlchemy session
37 sys.exit(-1)
56 self._init_session()
38 from traitlets.config.loader import Config
57
39 cfg = Config()
58 # imports, used in ipython shell
40 cfg.InteractiveShellEmbed.confirm_exit = False
59 import os
41 embed(config=cfg, banner1="Kallithea IShell.")
60 import sys
61 import time
62 import shutil
63 import datetime
64 from kallithea.model.db import *
65
66 try:
67 from IPython import embed
68 from IPython.config.loader import Config
69 cfg = Config()
70 cfg.InteractiveShellEmbed.confirm_exit = False
71 embed(config=cfg, banner1="Kallithea IShell.")
72 except ImportError:
73 print 'ipython installation required for ishell'
74 sys.exit(-1)
75
76 def update_parser(self):
77 pass
@@ -3,9 +3,9 b' api_url = http://kallithea.example.com/_'
3 api_user = admin
3 api_user = admin
4 api_key = XXXXXXXXXXXX
4 api_key = XXXXXXXXXXXX
5
5
6 ldap_uri = ldap://ldap.example.com:389
6 ldap_uri = ldaps://ldap.example.com:636
7 ldap_user = cn=kallithea,dc=example,dc=com
7 ldap_user = cn=kallithea,dc=example,dc=com
8 ldap_key = XXXXXXXXX
8 ldap_key = XXXXXXXXX
9 base_dn = dc=example,dc=com
9 base_dn = dc=example,dc=com
10
10
11 sync_users = True No newline at end of file
11 sync_users = True
@@ -207,7 +207,7 b' class LdapSync(object):'
207 groups = self.ldap_client.get_groups()
207 groups = self.ldap_client.get_groups()
208 for group in groups:
208 for group in groups:
209 try:
209 try:
210 self.kallithea_api.create_repo_group(group)
210 self.kallithea_api.create_group(group)
211 added += 1
211 added += 1
212 except Exception:
212 except Exception:
213 existing += 1
213 existing += 1
@@ -25,19 +25,21 b' Original author and date, and relevant c'
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 from kallithea.lib.utils2 import __get_lem
28 from kallithea.lib import pygmentsutils
29
29
30
30
31 # language map is also used by whoosh indexer, which for those specified
31 # language map is also used by whoosh indexer, which for those specified
32 # extensions will index it's content
32 # extensions will index it's content
33 LANGUAGES_EXTENSIONS_MAP = __get_lem()
33 LANGUAGES_EXTENSIONS_MAP = pygmentsutils.get_extension_descriptions()
34
35 # Whoosh index targets
34
36
35 #==============================================================================
37 # Extensions we want to index content of using whoosh
36 # WHOOSH INDEX EXTENSIONS
37 #==============================================================================
38 # EXTENSIONS WE WANT TO INDEX CONTENT OFF USING WHOOSH
39 INDEX_EXTENSIONS = LANGUAGES_EXTENSIONS_MAP.keys()
38 INDEX_EXTENSIONS = LANGUAGES_EXTENSIONS_MAP.keys()
40
39
40 # Filenames we want to index content of using whoosh
41 INDEX_FILENAMES = pygmentsutils.get_index_filenames()
42
41 # list of readme files to search in file tree and display in summary
43 # list of readme files to search in file tree and display in summary
42 # attached weights defines the search order lower is first
44 # attached weights defines the search order lower is first
43 ALL_READMES = [
45 ALL_READMES = [
@@ -65,7 +67,3 b' MARKDOWN_EXTS = ['
65 PLAIN_EXTS = [('.text', 2), ('.TEXT', 2)]
67 PLAIN_EXTS = [('.text', 2), ('.TEXT', 2)]
66
68
67 ALL_EXTS = MARKDOWN_EXTS + RST_EXTS + PLAIN_EXTS
69 ALL_EXTS = MARKDOWN_EXTS + RST_EXTS + PLAIN_EXTS
68
69 DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
70
71 DATE_FORMAT = "%Y-%m-%d"
@@ -11,130 +11,11 b''
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """WSGI environment setup for Kallithea."""
15 Pylons environment configuration
16 """
17
18 import os
19 import logging
20 import kallithea
21 import platform
22
23 import pylons
24 import mako.lookup
25 import beaker
26
27 # don't remove this import it does magic for celery
28 from kallithea.lib import celerypylons
29
30 import kallithea.lib.app_globals as app_globals
31
32 from kallithea.config.routing import make_map
33
34 from kallithea.lib import helpers
35 from kallithea.lib.auth import set_available_permissions
36 from kallithea.lib.utils import repo2db_mapper, make_ui, set_app_settings,\
37 load_rcextensions, check_git_version, set_vcs_config
38 from kallithea.lib.utils2 import engine_from_config, str2bool
39 from kallithea.lib.db_manage import DbManage
40 from kallithea.model import init_model
41 from kallithea.model.scm import ScmModel
42
43 log = logging.getLogger(__name__)
44
15
45
16 from kallithea.config.app_cfg import base_config
46 def load_environment(global_conf, app_conf, initial=False,
47 test_env=None, test_index=None):
48 """
49 Configure the Pylons environment via the ``pylons.config``
50 object
51 """
52 config = pylons.configuration.PylonsConfig()
53
54 # Pylons paths
55 root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
56 paths = dict(
57 root=root,
58 controllers=os.path.join(root, 'controllers'),
59 static_files=os.path.join(root, 'public'),
60 templates=[os.path.join(root, 'templates')]
61 )
62
63 # Initialize config with the basic options
64 config.init_app(global_conf, app_conf, package='kallithea', paths=paths)
65
66 # store some globals into kallithea
67 kallithea.CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
68 kallithea.CELERY_EAGER = str2bool(config['app_conf'].get('celery.always.eager'))
69
70 config['routes.map'] = make_map(config)
71 config['pylons.app_globals'] = app_globals.Globals(config)
72 config['pylons.h'] = helpers
73 kallithea.CONFIG = config
74
75 load_rcextensions(root_path=config['here'])
76
17
77 # Setup cache object as early as possible
18 __all__ = ['load_environment']
78 pylons.cache._push_object(config['pylons.app_globals'].cache)
79
80 # Create the Mako TemplateLookup, with the default auto-escaping
81 config['pylons.app_globals'].mako_lookup = mako.lookup.TemplateLookup(
82 directories=paths['templates'],
83 error_handler=pylons.error.handle_mako_error,
84 module_directory=os.path.join(app_conf['cache_dir'], 'templates'),
85 input_encoding='utf-8', default_filters=['escape'],
86 imports=['from webhelpers.html import escape'])
87
88 # sets the c attribute access when don't existing attribute are accessed
89 config['pylons.strict_tmpl_context'] = True
90 test = os.path.split(config['__file__'])[-1] == 'test.ini'
91 if test:
92 if test_env is None:
93 test_env = not int(os.environ.get('KALLITHEA_NO_TMP_PATH', 0))
94 if test_index is None:
95 test_index = not int(os.environ.get('KALLITHEA_WHOOSH_TEST_DISABLE', 0))
96 if os.environ.get('TEST_DB'):
97 # swap config if we pass enviroment variable
98 config['sqlalchemy.db1.url'] = os.environ.get('TEST_DB')
99
19
100 from kallithea.lib.utils import create_test_env, create_test_index
20 # Use base_config to setup the environment loader function
101 from kallithea.tests import TESTS_TMP_PATH
21 load_environment = base_config.make_load_environment()
102 #set KALLITHEA_NO_TMP_PATH=1 to disable re-creating the database and
103 #test repos
104 if test_env:
105 create_test_env(TESTS_TMP_PATH, config)
106 #set KALLITHEA_WHOOSH_TEST_DISABLE=1 to disable whoosh index during tests
107 if test_index:
108 create_test_index(TESTS_TMP_PATH, config, True)
109
110 DbManage.check_waitress()
111 # MULTIPLE DB configs
112 # Setup the SQLAlchemy database engine
113 sa_engine_db1 = engine_from_config(config, 'sqlalchemy.db1.')
114 init_model(sa_engine_db1)
115
116 set_available_permissions(config)
117 repos_path = make_ui('db').configitems('paths')[0][1]
118 config['base_path'] = repos_path
119 set_app_settings(config)
120
121 instance_id = kallithea.CONFIG.get('instance_id')
122 if instance_id == '*':
123 instance_id = '%s-%s' % (platform.uname()[1], os.getpid())
124 kallithea.CONFIG['instance_id'] = instance_id
125
126 # CONFIGURATION OPTIONS HERE (note: all config options will override
127 # any Pylons config options)
128
129 # store config reference into our module to skip import magic of
130 # pylons
131 kallithea.CONFIG.update(config)
132 set_vcs_config(kallithea.CONFIG)
133
134 #check git version
135 check_git_version()
136
137 if str2bool(config.get('initial_repo_scan', True)):
138 repo2db_mapper(ScmModel().repo_scan(repos_path),
139 remove_obsolete=False, install_git_hooks=False)
140 return config
@@ -11,102 +11,41 b''
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """WSGI middleware initialization for the Kallithea application."""
15 Pylons middleware initialization
15
16 """
16 import logging.config
17 from kallithea.config.app_cfg import base_config
18 from kallithea.config.environment import load_environment
19
20 __all__ = ['make_app']
17
21
18 from routes.middleware import RoutesMiddleware
22 # Use base_config to setup the necessary PasteDeploy application factory.
19 from paste.cascade import Cascade
23 # make_base_app will wrap the TurboGears2 app with all the middleware it needs.
20 from paste.registry import RegistryManager
24 make_base_app = base_config.setup_tg_wsgi_app(load_environment)
21 from paste.urlparser import StaticURLParser
22 from paste.deploy.converters import asbool
23 from paste.gzipper import make_gzip_middleware
24
25
25 from pylons.middleware import ErrorHandler, StatusCodeRedirect
26 from pylons.wsgiapp import PylonsApp
27
26
28 from kallithea.lib.middleware.simplehg import SimpleHg
27 def make_app_without_logging(global_conf, full_stack=True, **app_conf):
29 from kallithea.lib.middleware.simplegit import SimpleGit
28 """The core of make_app for use from gearbox commands (other than 'serve')"""
30 from kallithea.lib.middleware.https_fixup import HttpsFixup
29 return make_base_app(global_conf, full_stack=full_stack, **app_conf)
31 from kallithea.lib.middleware.sessionmiddleware import SecureSessionMiddleware
32 from kallithea.config.environment import load_environment
33 from kallithea.lib.middleware.wrapper import RequestWrapper
34
30
35
31
36 def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
32 def make_app(global_conf, full_stack=True, **app_conf):
37 """Create a Pylons WSGI application and return it
38
39 ``global_conf``
40 The inherited configuration for this application. Normally from
41 the [DEFAULT] section of the Paste ini file.
42
43 ``full_stack``
44 Whether or not this application provides a full WSGI stack (by
45 default, meaning it handles its own exceptions and errors).
46 Disable full_stack when this application is "managed" by
47 another WSGI middleware.
48
49 ``app_conf``
50 The application's local configuration. Normally specified in
51 the [app:<name>] section of the Paste ini file (where <name>
52 defaults to main).
53
54 """
33 """
55 # Configure the Pylons environment
34 Set up Kallithea with the settings found in the PasteDeploy configuration
56 config = load_environment(global_conf, app_conf)
35 file used.
57
58 # The Pylons WSGI app
59 app = PylonsApp(config=config)
60
61 # Routing/Session/Cache Middleware
62 app = RoutesMiddleware(app, config['routes.map'])
63 app = SecureSessionMiddleware(app, config)
64
65 # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)
66 if asbool(config['pdebug']):
67 from kallithea.lib.profiler import ProfilingMiddleware
68 app = ProfilingMiddleware(app)
69
70 if asbool(full_stack):
71
36
72 from kallithea.lib.middleware.sentry import Sentry
37 :param global_conf: The global settings for Kallithea (those
73 from kallithea.lib.middleware.appenlight import AppEnlight
38 defined under the ``[DEFAULT]`` section).
74 if AppEnlight and asbool(config['app_conf'].get('appenlight')):
39 :type global_conf: dict
75 app = AppEnlight(app, config)
40 :param full_stack: Should the whole TurboGears2 stack be set up?
76 elif Sentry:
41 :type full_stack: str or bool
77 app = Sentry(app, config)
42 :return: The Kallithea application with all the relevant middleware
78
43 loaded.
79 # Handle Python exceptions
80 app = ErrorHandler(app, global_conf, **config['pylons.errorware'])
81
82 # Display error documents for 401, 403, 404 status codes (and
83 # 500 when debug is disabled)
84 # Note: will buffer the output in memory!
85 if asbool(config['debug']):
86 app = StatusCodeRedirect(app)
87 else:
88 app = StatusCodeRedirect(app, [400, 401, 403, 404, 500])
89
44
90 # we want our low level middleware to get to the request ASAP. We don't
45 This is the PasteDeploy factory for the Kallithea application.
91 # need any pylons stack middleware in them - especially no StatusCodeRedirect buffering
92 app = SimpleHg(app, config)
93 app = SimpleGit(app, config)
94
95 # Enable https redirects based on HTTP_X_URL_SCHEME set by proxy
96 if any(asbool(config.get(x)) for x in ['https_fixup', 'force_https', 'use_htsts']):
97 app = HttpsFixup(app, config)
98
99 app = RequestWrapper(app, config) # logging
100
46
101 # Establish the Registry for this application
47 ``app_conf`` contains all the application-specific settings (those defined
102 app = RegistryManager(app) # thread / request-local module globals / variables
48 under ``[app:main]``.
103
49 """
104 if asbool(static_files):
50 logging.config.fileConfig(global_conf['__file__'])
105 # Serve static files
51 return make_app_without_logging(global_conf, full_stack=full_stack, **app_conf)
106 static_app = StaticURLParser(config['pylons.paths']['static_files'])
107 app = Cascade([static_app, app])
108 app = make_gzip_middleware(app, global_conf, compress_level=1)
109
110 app.config = config
111
112 return app
@@ -1,34 +1,35 b''
1 #!/usr/bin/env python2
1 """Kallithea Git hook
2
3 This hook is installed and maintained by Kallithea. It will be overwritten
4 by Kallithea - don't customize it manually!
5
6 When Kallithea invokes Git, the KALLITHEA_EXTRAS environment variable will
7 contain additional info like the Kallithea instance and user info that this
8 hook will use.
9 """
10
2 import os
11 import os
3 import sys
12 import sys
4
13
5 try:
14 # Set output mode on windows to binary for stderr.
6 import kallithea
15 # This prevents python (or the windows console) from replacing \n with \r\n.
7 KALLITHEA_HOOK_VER = '_TMPL_'
16 # Git doesn't display remote output lines that contain \r,
8 os.environ['KALLITHEA_HOOK_VER'] = KALLITHEA_HOOK_VER
17 # and therefore without this modification git would display empty lines
9 from kallithea.lib.hooks import handle_git_post_receive as _handler
18 # instead of the exception output.
10 except ImportError:
19 if sys.platform == "win32":
11 if os.environ.get('RC_DEBUG_GIT_HOOK'):
20 import msvcrt
12 import traceback
21 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
13 print traceback.format_exc()
22
14 kallithea = None
23 KALLITHEA_HOOK_VER = '_TMPL_'
24 os.environ['KALLITHEA_HOOK_VER'] = KALLITHEA_HOOK_VER
25 import kallithea.lib.hooks
15
26
16
27
17 def main():
28 def main():
18 if kallithea is None:
29 repo_path = os.path.abspath('.')
19 # exit with success if we cannot import kallithea !!
30 git_stdin_lines = sys.stdin.readlines()
20 # this allows simply push to this repo even without
31 sys.exit(kallithea.lib.hooks.handle_git_post_receive(repo_path, git_stdin_lines))
21 # kallithea
22 sys.exit(0)
23
32
24 repo_path = os.path.abspath('.')
25 push_data = sys.stdin.readlines()
26 # os.environ is modified here by a subprocess call that
27 # runs git and later git executes this hook.
28 # Environ gets some additional info from kallithea system
29 # like IP or username from basic-auth
30 _handler(repo_path, push_data, os.environ)
31 sys.exit(0)
32
33
33 if __name__ == '__main__':
34 if __name__ == '__main__':
34 main()
35 main()
@@ -1,34 +1,35 b''
1 #!/usr/bin/env python2
1 """Kallithea Git hook
2
3 This hook is installed and maintained by Kallithea. It will be overwritten
4 by Kallithea - don't customize it manually!
5
6 When Kallithea invokes Git, the KALLITHEA_EXTRAS environment variable will
7 contain additional info like the Kallithea instance and user info that this
8 hook will use.
9 """
10
2 import os
11 import os
3 import sys
12 import sys
4
13
5 try:
14 # Set output mode on windows to binary for stderr.
6 import kallithea
15 # This prevents python (or the windows console) from replacing \n with \r\n.
7 KALLITHEA_HOOK_VER = '_TMPL_'
16 # Git doesn't display remote output lines that contain \r,
8 os.environ['KALLITHEA_HOOK_VER'] = KALLITHEA_HOOK_VER
17 # and therefore without this modification git would display empty lines
9 from kallithea.lib.hooks import handle_git_pre_receive as _handler
18 # instead of the exception output.
10 except ImportError:
19 if sys.platform == "win32":
11 if os.environ.get('RC_DEBUG_GIT_HOOK'):
20 import msvcrt
12 import traceback
21 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
13 print traceback.format_exc()
22
14 kallithea = None
23 KALLITHEA_HOOK_VER = '_TMPL_'
24 os.environ['KALLITHEA_HOOK_VER'] = KALLITHEA_HOOK_VER
25 import kallithea.lib.hooks
15
26
16
27
17 def main():
28 def main():
18 if kallithea is None:
29 repo_path = os.path.abspath('.')
19 # exit with success if we cannot import kallithea !!
30 git_stdin_lines = sys.stdin.readlines()
20 # this allows simply push to this repo even without
31 sys.exit(kallithea.lib.hooks.handle_git_pre_receive(repo_path, git_stdin_lines))
21 # kallithea
22 sys.exit(0)
23
32
24 repo_path = os.path.abspath('.')
25 push_data = sys.stdin.readlines()
26 # os.environ is modified here by a subprocess call that
27 # runs git and later git executes this hook.
28 # Environ gets some additional info from kallithea system
29 # like IP or username from basic-auth
30 _handler(repo_path, push_data, os.environ)
31 sys.exit(0)
32
33
33 if __name__ == '__main__':
34 if __name__ == '__main__':
34 main()
35 main()
@@ -41,7 +41,7 b' def _crrepohook(*args, **kwargs):'
41 :param created_on:
41 :param created_on:
42 :param enable_downloads:
42 :param enable_downloads:
43 :param repo_id:
43 :param repo_id:
44 :param user_id:
44 :param owner_id:
45 :param enable_statistics:
45 :param enable_statistics:
46 :param clone_uri:
46 :param clone_uri:
47 :param fork_id:
47 :param fork_id:
@@ -49,6 +49,8 b' def _crrepohook(*args, **kwargs):'
49 :param created_by:
49 :param created_by:
50 """
50 """
51 return 0
51 return 0
52
53
52 CREATE_REPO_HOOK = _crrepohook
54 CREATE_REPO_HOOK = _crrepohook
53
55
54
56
@@ -73,6 +75,8 b' def _pre_cruserhook(*args, **kwargs):'
73 """
75 """
74 reason = 'allowed'
76 reason = 'allowed'
75 return True, reason
77 return True, reason
78
79
76 PRE_CREATE_USER_HOOK = _pre_cruserhook
80 PRE_CREATE_USER_HOOK = _pre_cruserhook
77
81
78 #==============================================================================
82 #==============================================================================
@@ -105,6 +109,8 b' def _cruserhook(*args, **kwargs):'
105 :param created_by:
109 :param created_by:
106 """
110 """
107 return 0
111 return 0
112
113
108 CREATE_USER_HOOK = _cruserhook
114 CREATE_USER_HOOK = _cruserhook
109
115
110
116
@@ -123,7 +129,7 b' def _dlrepohook(*args, **kwargs):'
123 :param created_on:
129 :param created_on:
124 :param enable_downloads:
130 :param enable_downloads:
125 :param repo_id:
131 :param repo_id:
126 :param user_id:
132 :param owner_id:
127 :param enable_statistics:
133 :param enable_statistics:
128 :param clone_uri:
134 :param clone_uri:
129 :param fork_id:
135 :param fork_id:
@@ -132,6 +138,8 b' def _dlrepohook(*args, **kwargs):'
132 :param deleted_on:
138 :param deleted_on:
133 """
139 """
134 return 0
140 return 0
141
142
135 DELETE_REPO_HOOK = _dlrepohook
143 DELETE_REPO_HOOK = _dlrepohook
136
144
137
145
@@ -165,6 +173,8 b' def _dluserhook(*args, **kwargs):'
165 :param deleted_by:
173 :param deleted_by:
166 """
174 """
167 return 0
175 return 0
176
177
168 DELETE_USER_HOOK = _dluserhook
178 DELETE_USER_HOOK = _dluserhook
169
179
170
180
@@ -189,6 +199,8 b' def _pushhook(*args, **kwargs):'
189 :param pushed_revs: list of pushed revisions
199 :param pushed_revs: list of pushed revisions
190 """
200 """
191 return 0
201 return 0
202
203
192 PUSH_HOOK = _pushhook
204 PUSH_HOOK = _pushhook
193
205
194
206
@@ -212,4 +224,6 b' def _pullhook(*args, **kwargs):'
212 :param repository: repository name
224 :param repository: repository name
213 """
225 """
214 return 0
226 return 0
227
228
215 PULL_HOOK = _pullhook
229 PULL_HOOK = _pullhook
@@ -19,6 +19,7 b' may take precedent over the more generic'
19 refer to the routes manual at http://routes.groovie.org/docs/
19 refer to the routes manual at http://routes.groovie.org/docs/
20 """
20 """
21
21
22 from tg import request
22 from routes import Mapper
23 from routes import Mapper
23
24
24 # prefix for non repository related links needs to be prefixed with `/`
25 # prefix for non repository related links needs to be prefixed with `/`
@@ -27,7 +28,7 b" ADMIN_PREFIX = '/_admin'"
27
28
28 def make_map(config):
29 def make_map(config):
29 """Create, configure and return the routes Mapper"""
30 """Create, configure and return the routes Mapper"""
30 rmap = Mapper(directory=config['pylons.paths']['controllers'],
31 rmap = Mapper(directory=config['paths']['controllers'],
31 always_scan=config['debug'])
32 always_scan=config['debug'])
32 rmap.minimization = False
33 rmap.minimization = False
33 rmap.explicit = False
34 rmap.explicit = False
@@ -45,7 +46,7 b' def make_map(config):'
45 repo_name = match_dict.get('repo_name')
46 repo_name = match_dict.get('repo_name')
46
47
47 if match_dict.get('f_path'):
48 if match_dict.get('f_path'):
48 #fix for multiple initial slashes that causes errors
49 # fix for multiple initial slashes that causes errors
49 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
50 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
50
51
51 by_id_match = get_repo_by_id(repo_name)
52 by_id_match = get_repo_by_id(repo_name)
@@ -89,20 +90,18 b' def make_map(config):'
89 def check_int(environ, match_dict):
90 def check_int(environ, match_dict):
90 return match_dict.get('id').isdigit()
91 return match_dict.get('id').isdigit()
91
92
92 # The ErrorController route (handles 404/500 error pages); it should
93 # likely stay at the top, ensuring it can always be resolved
94 rmap.connect('/error/{action}', controller='error')
95 rmap.connect('/error/{action}/{id}', controller='error')
96
97 #==========================================================================
93 #==========================================================================
98 # CUSTOM ROUTES HERE
94 # CUSTOM ROUTES HERE
99 #==========================================================================
95 #==========================================================================
100
96
101 #MAIN PAGE
97 # MAIN PAGE
102 rmap.connect('home', '/', controller='home', action='index')
98 rmap.connect('home', '/', controller='home', action='index')
103 rmap.connect('about', '/about', controller='home', action='about')
99 rmap.connect('about', '/about', controller='home', action='about')
100 rmap.redirect('/favicon.ico', '/images/favicon.ico')
104 rmap.connect('repo_switcher_data', '/_repos', controller='home',
101 rmap.connect('repo_switcher_data', '/_repos', controller='home',
105 action='repo_switcher_data')
102 action='repo_switcher_data')
103 rmap.connect('users_and_groups_data', '/_users_and_groups', controller='home',
104 action='users_and_groups_data')
106
105
107 rmap.connect('rst_help',
106 rmap.connect('rst_help',
108 "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
107 "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
@@ -110,7 +109,7 b' def make_map(config):'
110 rmap.connect('kallithea_project_url', "https://kallithea-scm.org/", _static=True)
109 rmap.connect('kallithea_project_url', "https://kallithea-scm.org/", _static=True)
111 rmap.connect('issues_url', 'https://bitbucket.org/conservancy/kallithea/issues', _static=True)
110 rmap.connect('issues_url', 'https://bitbucket.org/conservancy/kallithea/issues', _static=True)
112
111
113 #ADMIN REPOSITORY ROUTES
112 # ADMIN REPOSITORY ROUTES
114 with rmap.submapper(path_prefix=ADMIN_PREFIX,
113 with rmap.submapper(path_prefix=ADMIN_PREFIX,
115 controller='admin/repos') as m:
114 controller='admin/repos') as m:
116 m.connect("repos", "/repos",
115 m.connect("repos", "/repos",
@@ -119,14 +118,13 b' def make_map(config):'
119 action="index", conditions=dict(method=["GET"]))
118 action="index", conditions=dict(method=["GET"]))
120 m.connect("new_repo", "/create_repository",
119 m.connect("new_repo", "/create_repository",
121 action="create_repository", conditions=dict(method=["GET"]))
120 action="create_repository", conditions=dict(method=["GET"]))
122 m.connect("put_repo", "/repos/{repo_name:.*?}",
121 m.connect("update_repo", "/repos/{repo_name:.*?}",
123 action="update", conditions=dict(method=["PUT"],
122 action="update", conditions=dict(method=["POST"],
124 function=check_repo))
123 function=check_repo))
125 m.connect("delete_repo", "/repos/{repo_name:.*?}",
124 m.connect("delete_repo", "/repos/{repo_name:.*?}/delete",
126 action="delete", conditions=dict(method=["DELETE"],
125 action="delete", conditions=dict(method=["POST"]))
127 ))
128
126
129 #ADMIN REPOSITORY GROUPS ROUTES
127 # ADMIN REPOSITORY GROUPS ROUTES
130 with rmap.submapper(path_prefix=ADMIN_PREFIX,
128 with rmap.submapper(path_prefix=ADMIN_PREFIX,
131 controller='admin/repo_groups') as m:
129 controller='admin/repo_groups') as m:
132 m.connect("repos_groups", "/repo_groups",
130 m.connect("repos_groups", "/repo_groups",
@@ -136,47 +134,40 b' def make_map(config):'
136 m.connect("new_repos_group", "/repo_groups/new",
134 m.connect("new_repos_group", "/repo_groups/new",
137 action="new", conditions=dict(method=["GET"]))
135 action="new", conditions=dict(method=["GET"]))
138 m.connect("update_repos_group", "/repo_groups/{group_name:.*?}",
136 m.connect("update_repos_group", "/repo_groups/{group_name:.*?}",
139 action="update", conditions=dict(method=["PUT"],
137 action="update", conditions=dict(method=["POST"],
140 function=check_group))
138 function=check_group))
141
139
142 m.connect("repos_group", "/repo_groups/{group_name:.*?}",
140 m.connect("repos_group", "/repo_groups/{group_name:.*?}",
143 action="show", conditions=dict(method=["GET"],
141 action="show", conditions=dict(method=["GET"],
144 function=check_group))
142 function=check_group))
145
143
146 #EXTRAS REPO GROUP ROUTES
144 # EXTRAS REPO GROUP ROUTES
147 m.connect("edit_repo_group", "/repo_groups/{group_name:.*?}/edit",
145 m.connect("edit_repo_group", "/repo_groups/{group_name:.*?}/edit",
148 action="edit",
146 action="edit",
149 conditions=dict(method=["GET"], function=check_group))
147 conditions=dict(method=["GET"], function=check_group))
150 m.connect("edit_repo_group", "/repo_groups/{group_name:.*?}/edit",
151 action="edit",
152 conditions=dict(method=["PUT"], function=check_group))
153
148
154 m.connect("edit_repo_group_advanced", "/repo_groups/{group_name:.*?}/edit/advanced",
149 m.connect("edit_repo_group_advanced", "/repo_groups/{group_name:.*?}/edit/advanced",
155 action="edit_repo_group_advanced",
150 action="edit_repo_group_advanced",
156 conditions=dict(method=["GET"], function=check_group))
151 conditions=dict(method=["GET"], function=check_group))
157 m.connect("edit_repo_group_advanced", "/repo_groups/{group_name:.*?}/edit/advanced",
158 action="edit_repo_group_advanced",
159 conditions=dict(method=["PUT"], function=check_group))
160
152
161 m.connect("edit_repo_group_perms", "/repo_groups/{group_name:.*?}/edit/permissions",
153 m.connect("edit_repo_group_perms", "/repo_groups/{group_name:.*?}/edit/permissions",
162 action="edit_repo_group_perms",
154 action="edit_repo_group_perms",
163 conditions=dict(method=["GET"], function=check_group))
155 conditions=dict(method=["GET"], function=check_group))
164 m.connect("edit_repo_group_perms", "/repo_groups/{group_name:.*?}/edit/permissions",
156 m.connect("edit_repo_group_perms_update", "/repo_groups/{group_name:.*?}/edit/permissions",
165 action="update_perms",
157 action="update_perms",
166 conditions=dict(method=["PUT"], function=check_group))
158 conditions=dict(method=["POST"], function=check_group))
167 m.connect("edit_repo_group_perms", "/repo_groups/{group_name:.*?}/edit/permissions",
159 m.connect("edit_repo_group_perms_delete", "/repo_groups/{group_name:.*?}/edit/permissions/delete",
168 action="delete_perms",
160 action="delete_perms",
169 conditions=dict(method=["DELETE"], function=check_group))
161 conditions=dict(method=["POST"], function=check_group))
170
162
171 m.connect("delete_repo_group", "/repo_groups/{group_name:.*?}",
163 m.connect("delete_repo_group", "/repo_groups/{group_name:.*?}/delete",
172 action="delete", conditions=dict(method=["DELETE"],
164 action="delete", conditions=dict(method=["POST"],
173 function=check_group_skip_path))
165 function=check_group_skip_path))
174
166
175
167 # ADMIN USER ROUTES
176 #ADMIN USER ROUTES
177 with rmap.submapper(path_prefix=ADMIN_PREFIX,
168 with rmap.submapper(path_prefix=ADMIN_PREFIX,
178 controller='admin/users') as m:
169 controller='admin/users') as m:
179 m.connect("users", "/users",
170 m.connect("new_user", "/users/new",
180 action="create", conditions=dict(method=["POST"]))
171 action="create", conditions=dict(method=["POST"]))
181 m.connect("users", "/users",
172 m.connect("users", "/users",
182 action="index", conditions=dict(method=["GET"]))
173 action="index", conditions=dict(method=["GET"]))
@@ -185,47 +176,43 b' def make_map(config):'
185 m.connect("new_user", "/users/new",
176 m.connect("new_user", "/users/new",
186 action="new", conditions=dict(method=["GET"]))
177 action="new", conditions=dict(method=["GET"]))
187 m.connect("update_user", "/users/{id}",
178 m.connect("update_user", "/users/{id}",
188 action="update", conditions=dict(method=["PUT"]))
179 action="update", conditions=dict(method=["POST"]))
189 m.connect("delete_user", "/users/{id}",
180 m.connect("delete_user", "/users/{id}/delete",
190 action="delete", conditions=dict(method=["DELETE"]))
181 action="delete", conditions=dict(method=["POST"]))
191 m.connect("edit_user", "/users/{id}/edit",
182 m.connect("edit_user", "/users/{id}/edit",
192 action="edit", conditions=dict(method=["GET"]))
183 action="edit", conditions=dict(method=["GET"]))
193 m.connect("user", "/users/{id}",
194 action="show", conditions=dict(method=["GET"]))
195
184
196 #EXTRAS USER ROUTES
185 # EXTRAS USER ROUTES
197 m.connect("edit_user_advanced", "/users/{id}/edit/advanced",
186 m.connect("edit_user_advanced", "/users/{id}/edit/advanced",
198 action="edit_advanced", conditions=dict(method=["GET"]))
187 action="edit_advanced", conditions=dict(method=["GET"]))
199 m.connect("edit_user_advanced", "/users/{id}/edit/advanced",
200 action="update_advanced", conditions=dict(method=["PUT"]))
201
188
202 m.connect("edit_user_api_keys", "/users/{id}/edit/api_keys",
189 m.connect("edit_user_api_keys", "/users/{id}/edit/api_keys",
203 action="edit_api_keys", conditions=dict(method=["GET"]))
190 action="edit_api_keys", conditions=dict(method=["GET"]))
204 m.connect("edit_user_api_keys", "/users/{id}/edit/api_keys",
191 m.connect("edit_user_api_keys_update", "/users/{id}/edit/api_keys",
205 action="add_api_key", conditions=dict(method=["POST"]))
192 action="add_api_key", conditions=dict(method=["POST"]))
206 m.connect("edit_user_api_keys", "/users/{id}/edit/api_keys",
193 m.connect("edit_user_api_keys_delete", "/users/{id}/edit/api_keys/delete",
207 action="delete_api_key", conditions=dict(method=["DELETE"]))
194 action="delete_api_key", conditions=dict(method=["POST"]))
208
195
209 m.connect("edit_user_perms", "/users/{id}/edit/permissions",
196 m.connect("edit_user_perms", "/users/{id}/edit/permissions",
210 action="edit_perms", conditions=dict(method=["GET"]))
197 action="edit_perms", conditions=dict(method=["GET"]))
211 m.connect("edit_user_perms", "/users/{id}/edit/permissions",
198 m.connect("edit_user_perms_update", "/users/{id}/edit/permissions",
212 action="update_perms", conditions=dict(method=["PUT"]))
199 action="update_perms", conditions=dict(method=["POST"]))
213
200
214 m.connect("edit_user_emails", "/users/{id}/edit/emails",
201 m.connect("edit_user_emails", "/users/{id}/edit/emails",
215 action="edit_emails", conditions=dict(method=["GET"]))
202 action="edit_emails", conditions=dict(method=["GET"]))
216 m.connect("edit_user_emails", "/users/{id}/edit/emails",
203 m.connect("edit_user_emails_update", "/users/{id}/edit/emails",
217 action="add_email", conditions=dict(method=["PUT"]))
204 action="add_email", conditions=dict(method=["POST"]))
218 m.connect("edit_user_emails", "/users/{id}/edit/emails",
205 m.connect("edit_user_emails_delete", "/users/{id}/edit/emails/delete",
219 action="delete_email", conditions=dict(method=["DELETE"]))
206 action="delete_email", conditions=dict(method=["POST"]))
220
207
221 m.connect("edit_user_ips", "/users/{id}/edit/ips",
208 m.connect("edit_user_ips", "/users/{id}/edit/ips",
222 action="edit_ips", conditions=dict(method=["GET"]))
209 action="edit_ips", conditions=dict(method=["GET"]))
223 m.connect("edit_user_ips", "/users/{id}/edit/ips",
210 m.connect("edit_user_ips_update", "/users/{id}/edit/ips",
224 action="add_ip", conditions=dict(method=["PUT"]))
211 action="add_ip", conditions=dict(method=["POST"]))
225 m.connect("edit_user_ips", "/users/{id}/edit/ips",
212 m.connect("edit_user_ips_delete", "/users/{id}/edit/ips/delete",
226 action="delete_ip", conditions=dict(method=["DELETE"]))
213 action="delete_ip", conditions=dict(method=["POST"]))
227
214
228 #ADMIN USER GROUPS REST ROUTES
215 # ADMIN USER GROUPS REST ROUTES
229 with rmap.submapper(path_prefix=ADMIN_PREFIX,
216 with rmap.submapper(path_prefix=ADMIN_PREFIX,
230 controller='admin/user_groups') as m:
217 controller='admin/user_groups') as m:
231 m.connect("users_groups", "/user_groups",
218 m.connect("users_groups", "/user_groups",
@@ -235,28 +222,25 b' def make_map(config):'
235 m.connect("new_users_group", "/user_groups/new",
222 m.connect("new_users_group", "/user_groups/new",
236 action="new", conditions=dict(method=["GET"]))
223 action="new", conditions=dict(method=["GET"]))
237 m.connect("update_users_group", "/user_groups/{id}",
224 m.connect("update_users_group", "/user_groups/{id}",
238 action="update", conditions=dict(method=["PUT"]))
225 action="update", conditions=dict(method=["POST"]))
239 m.connect("delete_users_group", "/user_groups/{id}",
226 m.connect("delete_users_group", "/user_groups/{id}/delete",
240 action="delete", conditions=dict(method=["DELETE"]))
227 action="delete", conditions=dict(method=["POST"]))
241 m.connect("edit_users_group", "/user_groups/{id}/edit",
228 m.connect("edit_users_group", "/user_groups/{id}/edit",
242 action="edit", conditions=dict(method=["GET"]),
229 action="edit", conditions=dict(method=["GET"]),
243 function=check_user_group)
230 function=check_user_group)
244 m.connect("users_group", "/user_groups/{id}",
245 action="show", conditions=dict(method=["GET"]))
246
231
247 #EXTRAS USER GROUP ROUTES
232 # EXTRAS USER GROUP ROUTES
248 m.connect("edit_user_group_default_perms", "/user_groups/{id}/edit/default_perms",
233 m.connect("edit_user_group_default_perms", "/user_groups/{id}/edit/default_perms",
249 action="edit_default_perms", conditions=dict(method=["GET"]))
234 action="edit_default_perms", conditions=dict(method=["GET"]))
250 m.connect("edit_user_group_default_perms", "/user_groups/{id}/edit/default_perms",
235 m.connect("edit_user_group_default_perms_update", "/user_groups/{id}/edit/default_perms",
251 action="update_default_perms", conditions=dict(method=["PUT"]))
236 action="update_default_perms", conditions=dict(method=["POST"]))
252
253
237
254 m.connect("edit_user_group_perms", "/user_groups/{id}/edit/perms",
238 m.connect("edit_user_group_perms", "/user_groups/{id}/edit/perms",
255 action="edit_perms", conditions=dict(method=["GET"]))
239 action="edit_perms", conditions=dict(method=["GET"]))
256 m.connect("edit_user_group_perms", "/user_groups/{id}/edit/perms",
240 m.connect("edit_user_group_perms_update", "/user_groups/{id}/edit/perms",
257 action="update_perms", conditions=dict(method=["PUT"]))
241 action="update_perms", conditions=dict(method=["POST"]))
258 m.connect("edit_user_group_perms", "/user_groups/{id}/edit/perms",
242 m.connect("edit_user_group_perms_delete", "/user_groups/{id}/edit/perms/delete",
259 action="delete_perms", conditions=dict(method=["DELETE"]))
243 action="delete_perms", conditions=dict(method=["POST"]))
260
244
261 m.connect("edit_user_group_advanced", "/user_groups/{id}/edit/advanced",
245 m.connect("edit_user_group_advanced", "/user_groups/{id}/edit/advanced",
262 action="edit_advanced", conditions=dict(method=["GET"]))
246 action="edit_advanced", conditions=dict(method=["GET"]))
@@ -264,9 +248,7 b' def make_map(config):'
264 m.connect("edit_user_group_members", "/user_groups/{id}/edit/members",
248 m.connect("edit_user_group_members", "/user_groups/{id}/edit/members",
265 action="edit_members", conditions=dict(method=["GET"]))
249 action="edit_members", conditions=dict(method=["GET"]))
266
250
267
251 # ADMIN PERMISSIONS ROUTES
268
269 #ADMIN PERMISSIONS ROUTES
270 with rmap.submapper(path_prefix=ADMIN_PREFIX,
252 with rmap.submapper(path_prefix=ADMIN_PREFIX,
271 controller='admin/permissions') as m:
253 controller='admin/permissions') as m:
272 m.connect("admin_permissions", "/permissions",
254 m.connect("admin_permissions", "/permissions",
@@ -275,28 +257,27 b' def make_map(config):'
275 action="permission_globals", conditions=dict(method=["GET"]))
257 action="permission_globals", conditions=dict(method=["GET"]))
276
258
277 m.connect("admin_permissions_ips", "/permissions/ips",
259 m.connect("admin_permissions_ips", "/permissions/ips",
278 action="permission_ips", conditions=dict(method=["POST"]))
279 m.connect("admin_permissions_ips", "/permissions/ips",
280 action="permission_ips", conditions=dict(method=["GET"]))
260 action="permission_ips", conditions=dict(method=["GET"]))
281
261
282 m.connect("admin_permissions_perms", "/permissions/perms",
262 m.connect("admin_permissions_perms", "/permissions/perms",
283 action="permission_perms", conditions=dict(method=["POST"]))
284 m.connect("admin_permissions_perms", "/permissions/perms",
285 action="permission_perms", conditions=dict(method=["GET"]))
263 action="permission_perms", conditions=dict(method=["GET"]))
286
264
265 # ADMIN DEFAULTS ROUTES
266 with rmap.submapper(path_prefix=ADMIN_PREFIX,
267 controller='admin/defaults') as m:
268 m.connect('defaults', 'defaults',
269 action="index")
270 m.connect('defaults_update', 'defaults/{id}/update',
271 action="update", conditions=dict(method=["POST"]))
287
272
288 #ADMIN DEFAULTS REST ROUTES
273 # ADMIN AUTH SETTINGS
289 rmap.resource('default', 'defaults',
290 controller='admin/defaults', path_prefix=ADMIN_PREFIX)
291
292 #ADMIN AUTH SETTINGS
293 rmap.connect('auth_settings', '%s/auth' % ADMIN_PREFIX,
274 rmap.connect('auth_settings', '%s/auth' % ADMIN_PREFIX,
294 controller='admin/auth_settings', action='auth_settings',
275 controller='admin/auth_settings', action='auth_settings',
295 conditions=dict(method=["POST"]))
276 conditions=dict(method=["POST"]))
296 rmap.connect('auth_home', '%s/auth' % ADMIN_PREFIX,
277 rmap.connect('auth_home', '%s/auth' % ADMIN_PREFIX,
297 controller='admin/auth_settings')
278 controller='admin/auth_settings')
298
279
299 #ADMIN SETTINGS ROUTES
280 # ADMIN SETTINGS ROUTES
300 with rmap.submapper(path_prefix=ADMIN_PREFIX,
281 with rmap.submapper(path_prefix=ADMIN_PREFIX,
301 controller='admin/settings') as m:
282 controller='admin/settings') as m:
302 m.connect("admin_settings", "/settings",
283 m.connect("admin_settings", "/settings",
@@ -326,8 +307,8 b' def make_map(config):'
326
307
327 m.connect("admin_settings_hooks", "/settings/hooks",
308 m.connect("admin_settings_hooks", "/settings/hooks",
328 action="settings_hooks", conditions=dict(method=["POST"]))
309 action="settings_hooks", conditions=dict(method=["POST"]))
329 m.connect("admin_settings_hooks", "/settings/hooks",
310 m.connect("admin_settings_hooks_delete", "/settings/hooks/delete",
330 action="settings_hooks", conditions=dict(method=["DELETE"]))
311 action="settings_hooks", conditions=dict(method=["POST"]))
331 m.connect("admin_settings_hooks", "/settings/hooks",
312 m.connect("admin_settings_hooks", "/settings/hooks",
332 action="settings_hooks", conditions=dict(method=["GET"]))
313 action="settings_hooks", conditions=dict(method=["GET"]))
333
314
@@ -343,7 +324,7 b' def make_map(config):'
343 m.connect("admin_settings_system_update", "/settings/system/updates",
324 m.connect("admin_settings_system_update", "/settings/system/updates",
344 action="settings_system_update", conditions=dict(method=["GET"]))
325 action="settings_system_update", conditions=dict(method=["GET"]))
345
326
346 #ADMIN MY ACCOUNT
327 # ADMIN MY ACCOUNT
347 with rmap.submapper(path_prefix=ADMIN_PREFIX,
328 with rmap.submapper(path_prefix=ADMIN_PREFIX,
348 controller='admin/my_account') as m:
329 controller='admin/my_account') as m:
349
330
@@ -370,46 +351,17 b' def make_map(config):'
370 action="my_account_emails", conditions=dict(method=["GET"]))
351 action="my_account_emails", conditions=dict(method=["GET"]))
371 m.connect("my_account_emails", "/my_account/emails",
352 m.connect("my_account_emails", "/my_account/emails",
372 action="my_account_emails_add", conditions=dict(method=["POST"]))
353 action="my_account_emails_add", conditions=dict(method=["POST"]))
373 m.connect("my_account_emails", "/my_account/emails",
354 m.connect("my_account_emails_delete", "/my_account/emails/delete",
374 action="my_account_emails_delete", conditions=dict(method=["DELETE"]))
355 action="my_account_emails_delete", conditions=dict(method=["POST"]))
375
356
376 m.connect("my_account_api_keys", "/my_account/api_keys",
357 m.connect("my_account_api_keys", "/my_account/api_keys",
377 action="my_account_api_keys", conditions=dict(method=["GET"]))
358 action="my_account_api_keys", conditions=dict(method=["GET"]))
378 m.connect("my_account_api_keys", "/my_account/api_keys",
359 m.connect("my_account_api_keys", "/my_account/api_keys",
379 action="my_account_api_keys_add", conditions=dict(method=["POST"]))
360 action="my_account_api_keys_add", conditions=dict(method=["POST"]))
380 m.connect("my_account_api_keys", "/my_account/api_keys",
361 m.connect("my_account_api_keys_delete", "/my_account/api_keys/delete",
381 action="my_account_api_keys_delete", conditions=dict(method=["DELETE"]))
362 action="my_account_api_keys_delete", conditions=dict(method=["POST"]))
382
363
383 #NOTIFICATION REST ROUTES
364 # ADMIN GIST
384 with rmap.submapper(path_prefix=ADMIN_PREFIX,
385 controller='admin/notifications') as m:
386 m.connect("notifications", "/notifications",
387 action="create", conditions=dict(method=["POST"]))
388 m.connect("notifications", "/notifications",
389 action="index", conditions=dict(method=["GET"]))
390 m.connect("notifications_mark_all_read", "/notifications/mark_all_read",
391 action="mark_all_read", conditions=dict(method=["GET"]))
392 m.connect("formatted_notifications", "/notifications.{format}",
393 action="index", conditions=dict(method=["GET"]))
394 m.connect("new_notification", "/notifications/new",
395 action="new", conditions=dict(method=["GET"]))
396 m.connect("formatted_new_notification", "/notifications/new.{format}",
397 action="new", conditions=dict(method=["GET"]))
398 m.connect("/notifications/{notification_id}",
399 action="update", conditions=dict(method=["PUT"]))
400 m.connect("/notifications/{notification_id}",
401 action="delete", conditions=dict(method=["DELETE"]))
402 m.connect("edit_notification", "/notifications/{notification_id}/edit",
403 action="edit", conditions=dict(method=["GET"]))
404 m.connect("formatted_edit_notification",
405 "/notifications/{notification_id}.{format}/edit",
406 action="edit", conditions=dict(method=["GET"]))
407 m.connect("notification", "/notifications/{notification_id}",
408 action="show", conditions=dict(method=["GET"]))
409 m.connect("formatted_notification", "/notifications/{notification_id}.{format}",
410 action="show", conditions=dict(method=["GET"]))
411
412 #ADMIN GIST
413 with rmap.submapper(path_prefix=ADMIN_PREFIX,
365 with rmap.submapper(path_prefix=ADMIN_PREFIX,
414 controller='admin/gists') as m:
366 controller='admin/gists') as m:
415 m.connect("gists", "/gists",
367 m.connect("gists", "/gists",
@@ -419,17 +371,13 b' def make_map(config):'
419 m.connect("new_gist", "/gists/new",
371 m.connect("new_gist", "/gists/new",
420 action="new", conditions=dict(method=["GET"]))
372 action="new", conditions=dict(method=["GET"]))
421
373
422
374 m.connect("gist_delete", "/gists/{gist_id}/delete",
423 m.connect("/gists/{gist_id}",
375 action="delete", conditions=dict(method=["POST"]))
424 action="update", conditions=dict(method=["PUT"]))
425 m.connect("/gists/{gist_id}",
426 action="delete", conditions=dict(method=["DELETE"]))
427 m.connect("edit_gist", "/gists/{gist_id}/edit",
376 m.connect("edit_gist", "/gists/{gist_id}/edit",
428 action="edit", conditions=dict(method=["GET", "POST"]))
377 action="edit", conditions=dict(method=["GET", "POST"]))
429 m.connect("edit_gist_check_revision", "/gists/{gist_id}/edit/check_revision",
378 m.connect("edit_gist_check_revision", "/gists/{gist_id}/edit/check_revision",
430 action="check_revision", conditions=dict(method=["POST"]))
379 action="check_revision", conditions=dict(method=["POST"]))
431
380
432
433 m.connect("gist", "/gists/{gist_id}",
381 m.connect("gist", "/gists/{gist_id}",
434 action="show", conditions=dict(method=["GET"]))
382 action="show", conditions=dict(method=["GET"]))
435 m.connect("gist_rev", "/gists/{gist_id}/{revision}",
383 m.connect("gist_rev", "/gists/{gist_id}/{revision}",
@@ -442,7 +390,7 b' def make_map(config):'
442 revision='tip',
390 revision='tip',
443 action="show", conditions=dict(method=["GET"]))
391 action="show", conditions=dict(method=["GET"]))
444
392
445 #ADMIN MAIN PAGES
393 # ADMIN MAIN PAGES
446 with rmap.submapper(path_prefix=ADMIN_PREFIX,
394 with rmap.submapper(path_prefix=ADMIN_PREFIX,
447 controller='admin/admin') as m:
395 controller='admin/admin') as m:
448 m.connect('admin_home', '', action='index')
396 m.connect('admin_home', '', action='index')
@@ -451,11 +399,11 b' def make_map(config):'
451 #==========================================================================
399 #==========================================================================
452 # API V2
400 # API V2
453 #==========================================================================
401 #==========================================================================
454 with rmap.submapper(path_prefix=ADMIN_PREFIX,
402 with rmap.submapper(path_prefix=ADMIN_PREFIX, controller='api/api',
455 controller='api/api') as m:
403 action='_dispatch') as m:
456 m.connect('api', '/api')
404 m.connect('api', '/api')
457
405
458 #USER JOURNAL
406 # USER JOURNAL
459 rmap.connect('journal', '%s/journal' % ADMIN_PREFIX,
407 rmap.connect('journal', '%s/journal' % ADMIN_PREFIX,
460 controller='journal', action='index')
408 controller='journal', action='index')
461 rmap.connect('journal_rss', '%s/journal/rss' % ADMIN_PREFIX,
409 rmap.connect('journal_rss', '%s/journal/rss' % ADMIN_PREFIX,
@@ -484,7 +432,7 b' def make_map(config):'
484 controller='journal', action='toggle_following',
432 controller='journal', action='toggle_following',
485 conditions=dict(method=["POST"]))
433 conditions=dict(method=["POST"]))
486
434
487 #SEARCH
435 # SEARCH
488 rmap.connect('search', '%s/search' % ADMIN_PREFIX, controller='search',)
436 rmap.connect('search', '%s/search' % ADMIN_PREFIX, controller='search',)
489 rmap.connect('search_repo_admin', '%s/search/{repo_name:.*}' % ADMIN_PREFIX,
437 rmap.connect('search_repo_admin', '%s/search/{repo_name:.*}' % ADMIN_PREFIX,
490 controller='search',
438 controller='search',
@@ -494,7 +442,7 b' def make_map(config):'
494 conditions=dict(function=check_repo),
442 conditions=dict(function=check_repo),
495 )
443 )
496
444
497 #LOGIN/LOGOUT/REGISTER/SIGN IN
445 # LOGIN/LOGOUT/REGISTER/SIGN IN
498 rmap.connect('authentication_token', '%s/authentication_token' % ADMIN_PREFIX, controller='login', action='authentication_token')
446 rmap.connect('authentication_token', '%s/authentication_token' % ADMIN_PREFIX, controller='login', action='authentication_token')
499 rmap.connect('login_home', '%s/login' % ADMIN_PREFIX, controller='login')
447 rmap.connect('login_home', '%s/login' % ADMIN_PREFIX, controller='login')
500 rmap.connect('logout_home', '%s/logout' % ADMIN_PREFIX, controller='login',
448 rmap.connect('logout_home', '%s/logout' % ADMIN_PREFIX, controller='login',
@@ -510,7 +458,7 b' def make_map(config):'
510 '%s/password_reset_confirmation' % ADMIN_PREFIX,
458 '%s/password_reset_confirmation' % ADMIN_PREFIX,
511 controller='login', action='password_reset_confirmation')
459 controller='login', action='password_reset_confirmation')
512
460
513 #FEEDS
461 # FEEDS
514 rmap.connect('rss_feed_home', '/{repo_name:.*?}/feed/rss',
462 rmap.connect('rss_feed_home', '/{repo_name:.*?}/feed/rss',
515 controller='feed', action='rss',
463 controller='feed', action='rss',
516 conditions=dict(function=check_repo))
464 conditions=dict(function=check_repo))
@@ -543,8 +491,6 b' def make_map(config):'
543 controller='summary', action='repo_size',
491 controller='summary', action='repo_size',
544 conditions=dict(function=check_repo))
492 conditions=dict(function=check_repo))
545
493
546 rmap.connect('branch_tag_switcher', '/{repo_name:.*?}/branches-tags',
547 controller='home', action='branch_tag_switcher')
548 rmap.connect('repo_refs_data', '/{repo_name:.*?}/refs-data',
494 rmap.connect('repo_refs_data', '/{repo_name:.*?}/refs-data',
549 controller='home', action='repo_refs_data')
495 controller='home', action='repo_refs_data')
550
496
@@ -568,21 +514,20 b' def make_map(config):'
568 conditions=dict(method=["GET"], function=check_repo))
514 conditions=dict(method=["GET"], function=check_repo))
569 rmap.connect("edit_repo_perms_update", "/{repo_name:.*?}/settings/permissions",
515 rmap.connect("edit_repo_perms_update", "/{repo_name:.*?}/settings/permissions",
570 controller='admin/repos', action="edit_permissions_update",
516 controller='admin/repos', action="edit_permissions_update",
571 conditions=dict(method=["PUT"], function=check_repo))
517 conditions=dict(method=["POST"], function=check_repo))
572 rmap.connect("edit_repo_perms_revoke", "/{repo_name:.*?}/settings/permissions",
518 rmap.connect("edit_repo_perms_revoke", "/{repo_name:.*?}/settings/permissions/delete",
573 controller='admin/repos', action="edit_permissions_revoke",
519 controller='admin/repos', action="edit_permissions_revoke",
574 conditions=dict(method=["DELETE"], function=check_repo))
520 conditions=dict(method=["POST"], function=check_repo))
575
521
576 rmap.connect("edit_repo_fields", "/{repo_name:.*?}/settings/fields",
522 rmap.connect("edit_repo_fields", "/{repo_name:.*?}/settings/fields",
577 controller='admin/repos', action="edit_fields",
523 controller='admin/repos', action="edit_fields",
578 conditions=dict(method=["GET"], function=check_repo))
524 conditions=dict(method=["GET"], function=check_repo))
579 rmap.connect('create_repo_fields', "/{repo_name:.*?}/settings/fields/new",
525 rmap.connect('create_repo_fields', "/{repo_name:.*?}/settings/fields/new",
580 controller='admin/repos', action="create_repo_field",
526 controller='admin/repos', action="create_repo_field",
581 conditions=dict(method=["PUT"], function=check_repo))
527 conditions=dict(method=["POST"], function=check_repo))
582 rmap.connect('delete_repo_fields', "/{repo_name:.*?}/settings/fields/{field_id}",
528 rmap.connect('delete_repo_fields', "/{repo_name:.*?}/settings/fields/{field_id}/delete",
583 controller='admin/repos', action="delete_repo_field",
529 controller='admin/repos', action="delete_repo_field",
584 conditions=dict(method=["DELETE"], function=check_repo))
530 conditions=dict(method=["POST"], function=check_repo))
585
586
531
587 rmap.connect("edit_repo_advanced", "/{repo_name:.*?}/settings/advanced",
532 rmap.connect("edit_repo_advanced", "/{repo_name:.*?}/settings/advanced",
588 controller='admin/repos', action="edit_advanced",
533 controller='admin/repos', action="edit_advanced",
@@ -590,43 +535,41 b' def make_map(config):'
590
535
591 rmap.connect("edit_repo_advanced_locking", "/{repo_name:.*?}/settings/advanced/locking",
536 rmap.connect("edit_repo_advanced_locking", "/{repo_name:.*?}/settings/advanced/locking",
592 controller='admin/repos', action="edit_advanced_locking",
537 controller='admin/repos', action="edit_advanced_locking",
593 conditions=dict(method=["PUT"], function=check_repo))
538 conditions=dict(method=["POST"], function=check_repo))
594 rmap.connect('toggle_locking', "/{repo_name:.*?}/settings/advanced/locking_toggle",
539 rmap.connect('toggle_locking', "/{repo_name:.*?}/settings/advanced/locking_toggle",
595 controller='admin/repos', action="toggle_locking",
540 controller='admin/repos', action="toggle_locking",
596 conditions=dict(method=["GET"], function=check_repo))
541 conditions=dict(method=["GET"], function=check_repo))
597
542
598 rmap.connect("edit_repo_advanced_journal", "/{repo_name:.*?}/settings/advanced/journal",
543 rmap.connect("edit_repo_advanced_journal", "/{repo_name:.*?}/settings/advanced/journal",
599 controller='admin/repos', action="edit_advanced_journal",
544 controller='admin/repos', action="edit_advanced_journal",
600 conditions=dict(method=["PUT"], function=check_repo))
545 conditions=dict(method=["POST"], function=check_repo))
601
546
602 rmap.connect("edit_repo_advanced_fork", "/{repo_name:.*?}/settings/advanced/fork",
547 rmap.connect("edit_repo_advanced_fork", "/{repo_name:.*?}/settings/advanced/fork",
603 controller='admin/repos', action="edit_advanced_fork",
548 controller='admin/repos', action="edit_advanced_fork",
604 conditions=dict(method=["PUT"], function=check_repo))
549 conditions=dict(method=["POST"], function=check_repo))
605
606
550
607 rmap.connect("edit_repo_caches", "/{repo_name:.*?}/settings/caches",
551 rmap.connect("edit_repo_caches", "/{repo_name:.*?}/settings/caches",
608 controller='admin/repos', action="edit_caches",
552 controller='admin/repos', action="edit_caches",
609 conditions=dict(method=["GET"], function=check_repo))
553 conditions=dict(method=["GET"], function=check_repo))
610 rmap.connect("edit_repo_caches", "/{repo_name:.*?}/settings/caches",
554 rmap.connect("update_repo_caches", "/{repo_name:.*?}/settings/caches",
611 controller='admin/repos', action="edit_caches",
555 controller='admin/repos', action="edit_caches",
612 conditions=dict(method=["PUT"], function=check_repo))
556 conditions=dict(method=["POST"], function=check_repo))
613
614
557
615 rmap.connect("edit_repo_remote", "/{repo_name:.*?}/settings/remote",
558 rmap.connect("edit_repo_remote", "/{repo_name:.*?}/settings/remote",
616 controller='admin/repos', action="edit_remote",
559 controller='admin/repos', action="edit_remote",
617 conditions=dict(method=["GET"], function=check_repo))
560 conditions=dict(method=["GET"], function=check_repo))
618 rmap.connect("edit_repo_remote", "/{repo_name:.*?}/settings/remote",
561 rmap.connect("edit_repo_remote_update", "/{repo_name:.*?}/settings/remote",
619 controller='admin/repos', action="edit_remote",
562 controller='admin/repos', action="edit_remote",
620 conditions=dict(method=["PUT"], function=check_repo))
563 conditions=dict(method=["POST"], function=check_repo))
621
564
622 rmap.connect("edit_repo_statistics", "/{repo_name:.*?}/settings/statistics",
565 rmap.connect("edit_repo_statistics", "/{repo_name:.*?}/settings/statistics",
623 controller='admin/repos', action="edit_statistics",
566 controller='admin/repos', action="edit_statistics",
624 conditions=dict(method=["GET"], function=check_repo))
567 conditions=dict(method=["GET"], function=check_repo))
625 rmap.connect("edit_repo_statistics", "/{repo_name:.*?}/settings/statistics",
568 rmap.connect("edit_repo_statistics_update", "/{repo_name:.*?}/settings/statistics",
626 controller='admin/repos', action="edit_statistics",
569 controller='admin/repos', action="edit_statistics",
627 conditions=dict(method=["PUT"], function=check_repo))
570 conditions=dict(method=["POST"], function=check_repo))
628
571
629 #still working url for backward compat.
572 # still working url for backward compat.
630 rmap.connect('raw_changeset_home_depraced',
573 rmap.connect('raw_changeset_home_depraced',
631 '/{repo_name:.*?}/raw-changeset/{revision}',
574 '/{repo_name:.*?}/raw-changeset/{revision}',
632 controller='changeset', action='changeset_raw',
575 controller='changeset', action='changeset_raw',
@@ -653,16 +596,11 b' def make_map(config):'
653 controller='changeset', revision='tip', action='comment',
596 controller='changeset', revision='tip', action='comment',
654 conditions=dict(function=check_repo))
597 conditions=dict(function=check_repo))
655
598
656 rmap.connect('changeset_comment_preview',
599 rmap.connect('changeset_comment_delete',
657 '/{repo_name:.*?}/changeset-comment-preview',
600 '/{repo_name:.*?}/changeset-comment/{comment_id}/delete',
658 controller='changeset', action='preview_comment',
601 controller='changeset', action='delete_comment',
659 conditions=dict(function=check_repo, method=["POST"]))
602 conditions=dict(function=check_repo, method=["POST"]))
660
603
661 rmap.connect('changeset_comment_delete',
662 '/{repo_name:.*?}/changeset-comment-delete/{comment_id}',
663 controller='changeset', action='delete_comment',
664 conditions=dict(function=check_repo, method=["DELETE"]))
665
666 rmap.connect('changeset_info', '/changeset_info/{repo_name:.*?}/{revision}',
604 rmap.connect('changeset_info', '/changeset_info/{repo_name:.*?}/{revision}',
667 controller='changeset', action='changeset_info')
605 controller='changeset', action='changeset_info')
668
606
@@ -706,10 +644,10 b' def make_map(config):'
706 action='post', conditions=dict(function=check_repo,
644 action='post', conditions=dict(function=check_repo,
707 method=["POST"]))
645 method=["POST"]))
708 rmap.connect('pullrequest_delete',
646 rmap.connect('pullrequest_delete',
709 '/{repo_name:.*?}/pull-request/{pull_request_id}',
647 '/{repo_name:.*?}/pull-request/{pull_request_id}/delete',
710 controller='pullrequests',
648 controller='pullrequests',
711 action='delete', conditions=dict(function=check_repo,
649 action='delete', conditions=dict(function=check_repo,
712 method=["DELETE"]))
650 method=["POST"]))
713
651
714 rmap.connect('pullrequest_show_all',
652 rmap.connect('pullrequest_show_all',
715 '/{repo_name:.*?}/pull-request',
653 '/{repo_name:.*?}/pull-request',
@@ -731,27 +669,14 b' def make_map(config):'
731 rmap.connect('pullrequest_comment_delete',
669 rmap.connect('pullrequest_comment_delete',
732 '/{repo_name:.*?}/pull-request-comment/{comment_id}/delete',
670 '/{repo_name:.*?}/pull-request-comment/{comment_id}/delete',
733 controller='pullrequests', action='delete_comment',
671 controller='pullrequests', action='delete_comment',
734 conditions=dict(function=check_repo, method=["DELETE"]))
672 conditions=dict(function=check_repo, method=["POST"]))
735
673
736 rmap.connect('summary_home_summary', '/{repo_name:.*?}/summary',
674 rmap.connect('summary_home_summary', '/{repo_name:.*?}/summary',
737 controller='summary', conditions=dict(function=check_repo))
675 controller='summary', conditions=dict(function=check_repo))
738
676
739 rmap.connect('branches_home', '/{repo_name:.*?}/branches',
740 controller='branches', conditions=dict(function=check_repo))
741
742 rmap.connect('tags_home', '/{repo_name:.*?}/tags',
743 controller='tags', conditions=dict(function=check_repo))
744
745 rmap.connect('bookmarks_home', '/{repo_name:.*?}/bookmarks',
746 controller='bookmarks', conditions=dict(function=check_repo))
747
748 rmap.connect('changelog_home', '/{repo_name:.*?}/changelog',
677 rmap.connect('changelog_home', '/{repo_name:.*?}/changelog',
749 controller='changelog', conditions=dict(function=check_repo))
678 controller='changelog', conditions=dict(function=check_repo))
750
679
751 rmap.connect('changelog_summary_home', '/{repo_name:.*?}/changelog_summary',
752 controller='changelog', action='changelog_summary',
753 conditions=dict(function=check_repo))
754
755 rmap.connect('changelog_file_home', '/{repo_name:.*?}/changelog/{revision}/{f_path:.*}',
680 rmap.connect('changelog_file_home', '/{repo_name:.*?}/changelog/{revision}/{f_path:.*}',
756 controller='changelog', f_path=None,
681 controller='changelog', f_path=None,
757 conditions=dict(function=check_repo))
682 conditions=dict(function=check_repo))
@@ -842,3 +767,29 b' def make_map(config):'
842 conditions=dict(function=check_repo))
767 conditions=dict(function=check_repo))
843
768
844 return rmap
769 return rmap
770
771
772 class UrlGenerator(object):
773 """Emulate pylons.url in providing a wrapper around routes.url
774
775 This code was added during migration from Pylons to Turbogears2. Pylons
776 already provided a wrapper like this, but Turbogears2 does not.
777
778 When the routing of Kallithea is changed to use less Routes and more
779 Turbogears2-style routing, this class may disappear or change.
780
781 url() (the __call__ method) returns the URL based on a route name and
782 arguments.
783 url.current() returns the URL of the current page with arguments applied.
784
785 Refer to documentation of Routes for details:
786 https://routes.readthedocs.io/en/latest/generating.html#generation
787 """
788 def __call__(self, *args, **kwargs):
789 return request.environ['routes.url'](*args, **kwargs)
790
791 def current(self, *args, **kwargs):
792 return request.environ['routes.url'].current(*args, **kwargs)
793
794
795 url = UrlGenerator()
@@ -28,19 +28,20 b' Original author and date, and relevant c'
28
28
29 import logging
29 import logging
30
30
31 from pylons import request, tmpl_context as c, url
31 from tg import request, tmpl_context as c
32 from sqlalchemy.orm import joinedload
32 from sqlalchemy.orm import joinedload
33 from whoosh.qparser.default import QueryParser
33 from whoosh.qparser.default import QueryParser
34 from whoosh.qparser.dateparse import DateParserPlugin
34 from whoosh.qparser.dateparse import DateParserPlugin
35 from whoosh import query
35 from whoosh import query
36 from sqlalchemy.sql.expression import or_, and_, func
36 from sqlalchemy.sql.expression import or_, and_, func
37
37
38 from kallithea.config.routing import url
38 from kallithea.model.db import UserLog
39 from kallithea.model.db import UserLog
39 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
40 from kallithea.lib.auth import LoginRequired, HasPermissionAnyDecorator
40 from kallithea.lib.base import BaseController, render
41 from kallithea.lib.base import BaseController, render
41 from kallithea.lib.utils2 import safe_int, remove_prefix, remove_suffix
42 from kallithea.lib.utils2 import safe_int, remove_prefix, remove_suffix
42 from kallithea.lib.indexers import JOURNAL_SCHEMA
43 from kallithea.lib.indexers import JOURNAL_SCHEMA
43 from kallithea.lib.helpers import Page
44 from kallithea.lib.page import Page
44
45
45
46
46 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
@@ -64,14 +65,14 b' def _journal_filter(user_log, search_ter'
64
65
65 def wildcard_handler(col, wc_term):
66 def wildcard_handler(col, wc_term):
66 if wc_term.startswith('*') and not wc_term.endswith('*'):
67 if wc_term.startswith('*') and not wc_term.endswith('*'):
67 #postfix == endswith
68 # postfix == endswith
68 wc_term = remove_prefix(wc_term, prefix='*')
69 wc_term = remove_prefix(wc_term, prefix='*')
69 return func.lower(col).endswith(wc_term)
70 return func.lower(col).endswith(func.lower(wc_term))
70 elif wc_term.startswith('*') and wc_term.endswith('*'):
71 elif wc_term.startswith('*') and wc_term.endswith('*'):
71 #wildcard == ilike
72 # wildcard == ilike
72 wc_term = remove_prefix(wc_term, prefix='*')
73 wc_term = remove_prefix(wc_term, prefix='*')
73 wc_term = remove_suffix(wc_term, suffix='*')
74 wc_term = remove_suffix(wc_term, suffix='*')
74 return func.lower(col).contains(wc_term)
75 return func.lower(col).contains(func.lower(wc_term))
75
76
76 def get_filterion(field, val, term):
77 def get_filterion(field, val, term):
77
78
@@ -87,7 +88,7 b' def _journal_filter(user_log, search_ter'
87 field = getattr(UserLog, field)
88 field = getattr(UserLog, field)
88 log.debug('filter field: %s val=>%s', field, val)
89 log.debug('filter field: %s val=>%s', field, val)
89
90
90 #sql filtering
91 # sql filtering
91 if isinstance(term, query.Wildcard):
92 if isinstance(term, query.Wildcard):
92 return wildcard_handler(field, val)
93 return wildcard_handler(field, val)
93 elif isinstance(term, query.Prefix):
94 elif isinstance(term, query.Prefix):
@@ -119,23 +120,23 b' def _journal_filter(user_log, search_ter'
119
120
120 class AdminController(BaseController):
121 class AdminController(BaseController):
121
122
122 @LoginRequired()
123 @LoginRequired(allow_default_user=True)
123 def __before__(self):
124 def _before(self, *args, **kwargs):
124 super(AdminController, self).__before__()
125 super(AdminController, self)._before(*args, **kwargs)
125
126
126 @HasPermissionAllDecorator('hg.admin')
127 @HasPermissionAnyDecorator('hg.admin')
127 def index(self):
128 def index(self):
128 users_log = UserLog.query()\
129 users_log = UserLog.query() \
129 .options(joinedload(UserLog.user))\
130 .options(joinedload(UserLog.user)) \
130 .options(joinedload(UserLog.repository))
131 .options(joinedload(UserLog.repository))
131
132
132 #FILTERING
133 # FILTERING
133 c.search_term = request.GET.get('filter')
134 c.search_term = request.GET.get('filter')
134 users_log = _journal_filter(users_log, c.search_term)
135 users_log = _journal_filter(users_log, c.search_term)
135
136
136 users_log = users_log.order_by(UserLog.action_date.desc())
137 users_log = users_log.order_by(UserLog.action_date.desc())
137
138
138 p = safe_int(request.GET.get('page', 1), 1)
139 p = safe_int(request.GET.get('page'), 1)
139
140
140 def url_generator(**kw):
141 def url_generator(**kw):
141 return url.current(filter=c.search_term, **kw)
142 return url.current(filter=c.search_term, **kw)
@@ -27,14 +27,15 b' import logging'
27 import formencode.htmlfill
27 import formencode.htmlfill
28 import traceback
28 import traceback
29
29
30 from pylons import request, tmpl_context as c, url
30 from tg import request, tmpl_context as c
31 from pylons.controllers.util import redirect
31 from tg.i18n import ugettext as _
32 from pylons.i18n.translation import _
32 from webob.exc import HTTPFound
33
33
34 from kallithea.config.routing import url
34 from kallithea.lib import helpers as h
35 from kallithea.lib import helpers as h
35 from kallithea.lib.compat import formatted_json
36 from kallithea.lib.compat import formatted_json
36 from kallithea.lib.base import BaseController, render
37 from kallithea.lib.base import BaseController, render
37 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
38 from kallithea.lib.auth import LoginRequired, HasPermissionAnyDecorator
38 from kallithea.lib import auth_modules
39 from kallithea.lib import auth_modules
39 from kallithea.model.forms import AuthSettingsForm
40 from kallithea.model.forms import AuthSettingsForm
40 from kallithea.model.db import Setting
41 from kallithea.model.db import Setting
@@ -46,9 +47,9 b' log = logging.getLogger(__name__)'
46 class AuthSettingsController(BaseController):
47 class AuthSettingsController(BaseController):
47
48
48 @LoginRequired()
49 @LoginRequired()
49 @HasPermissionAllDecorator('hg.admin')
50 @HasPermissionAnyDecorator('hg.admin')
50 def __before__(self):
51 def _before(self, *args, **kwargs):
51 super(AuthSettingsController, self).__before__()
52 super(AuthSettingsController, self)._before(*args, **kwargs)
52
53
53 def __load_defaults(self):
54 def __load_defaults(self):
54 c.available_plugins = [
55 c.available_plugins = [
@@ -58,32 +59,32 b' class AuthSettingsController(BaseControl'
58 'kallithea.lib.auth_modules.auth_crowd',
59 'kallithea.lib.auth_modules.auth_crowd',
59 'kallithea.lib.auth_modules.auth_pam'
60 'kallithea.lib.auth_modules.auth_pam'
60 ]
61 ]
61 c.enabled_plugins = Setting.get_auth_plugins()
62 self.enabled_plugins = auth_modules.get_auth_plugins()
63 c.enabled_plugin_names = [plugin.__class__.__module__ for plugin in self.enabled_plugins]
62
64
63 def __render(self, defaults, errors):
65 def __render(self, defaults, errors):
64 c.defaults = {}
66 c.defaults = {}
65 c.plugin_settings = {}
67 c.plugin_settings = {}
66 c.plugin_shortnames = {}
68 c.plugin_shortnames = {}
67
69
68 for module in c.enabled_plugins:
70 for plugin in self.enabled_plugins:
69 plugin = auth_modules.loadplugin(module)
71 module = plugin.__class__.__module__
70 plugin_name = plugin.name
72 c.plugin_shortnames[module] = plugin.name
71 c.plugin_shortnames[module] = plugin_name
72 c.plugin_settings[module] = plugin.plugin_settings()
73 c.plugin_settings[module] = plugin.plugin_settings()
73 for v in c.plugin_settings[module]:
74 for v in c.plugin_settings[module]:
74 fullname = ("auth_" + plugin_name + "_" + v["name"])
75 fullname = "auth_%s_%s" % (plugin.name, v["name"])
75 if "default" in v:
76 if "default" in v:
76 c.defaults[fullname] = v["default"]
77 c.defaults[fullname] = v["default"]
77 # Current values will be the default on the form, if there are any
78 # Current values will be the default on the form, if there are any
78 setting = Setting.get_by_name(fullname)
79 setting = Setting.get_by_name(fullname)
79 if setting is not None:
80 if setting is not None:
80 c.defaults[fullname] = setting.app_settings_value
81 c.defaults[fullname] = setting.app_settings_value
81 # we want to show , separated list of enabled plugins
82 c.defaults['auth_plugins'] = ','.join(c.enabled_plugins)
83
84 if defaults:
82 if defaults:
85 c.defaults.update(defaults)
83 c.defaults.update(defaults)
86
84
85 # we want to show , separated list of enabled plugins
86 c.defaults['auth_plugins'] = ','.join(c.enabled_plugin_names)
87
87 log.debug(formatted_json(defaults))
88 log.debug(formatted_json(defaults))
88 return formencode.htmlfill.render(
89 return formencode.htmlfill.render(
89 render('admin/auth/auth_settings.html'),
90 render('admin/auth/auth_settings.html'),
@@ -117,10 +118,10 b' class AuthSettingsController(BaseControl'
117 # (yet), since that'll cause validation errors and/or wrong
118 # (yet), since that'll cause validation errors and/or wrong
118 # settings being applied (e.g. checkboxes being cleared),
119 # settings being applied (e.g. checkboxes being cleared),
119 # since the plugin settings will not be in the POST data.
120 # since the plugin settings will not be in the POST data.
120 c.enabled_plugins = [ p for p in c.enabled_plugins if p in new_enabled_plugins ]
121 c.enabled_plugin_names = [p for p in c.enabled_plugin_names if p in new_enabled_plugins]
121
122
122 # Next, parse everything including plugin settings.
123 # Next, parse everything including plugin settings.
123 _form = AuthSettingsForm(c.enabled_plugins)()
124 _form = AuthSettingsForm(c.enabled_plugin_names)()
124
125
125 try:
126 try:
126 form_result = _form.to_python(dict(request.POST))
127 form_result = _form.to_python(dict(request.POST))
@@ -130,7 +131,6 b' class AuthSettingsController(BaseControl'
130 v = ','.join(v)
131 v = ','.join(v)
131 log.debug("%s = %s", k, str(v))
132 log.debug("%s = %s", k, str(v))
132 setting = Setting.create_or_update(k, v)
133 setting = Setting.create_or_update(k, v)
133 Session().add(setting)
134 Session().commit()
134 Session().commit()
135 h.flash(_('Auth settings updated successfully'),
135 h.flash(_('Auth settings updated successfully'),
136 category='success')
136 category='success')
@@ -146,4 +146,4 b' class AuthSettingsController(BaseControl'
146 h.flash(_('error occurred during update of auth settings'),
146 h.flash(_('error occurred during update of auth settings'),
147 category='error')
147 category='error')
148
148
149 return redirect(url('auth_home'))
149 raise HTTPFound(location=url('auth_home'))
@@ -30,12 +30,13 b' import traceback'
30 import formencode
30 import formencode
31 from formencode import htmlfill
31 from formencode import htmlfill
32
32
33 from pylons import request, tmpl_context as c, url
33 from tg import request, tmpl_context as c
34 from pylons.controllers.util import redirect
34 from tg.i18n import ugettext as _
35 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound
36
36
37 from kallithea.config.routing import url
37 from kallithea.lib import helpers as h
38 from kallithea.lib import helpers as h
38 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
39 from kallithea.lib.auth import LoginRequired, HasPermissionAnyDecorator
39 from kallithea.lib.base import BaseController, render
40 from kallithea.lib.base import BaseController, render
40 from kallithea.model.forms import DefaultsForm
41 from kallithea.model.forms import DefaultsForm
41 from kallithea.model.meta import Session
42 from kallithea.model.meta import Session
@@ -46,19 +47,13 b' log = logging.getLogger(__name__)'
46
47
47
48
48 class DefaultsController(BaseController):
49 class DefaultsController(BaseController):
49 """REST Controller styled on the Atom Publishing Protocol"""
50 # To properly map this controller, ensure your config/routing.py
51 # file has a resource setup:
52 # map.resource('default', 'defaults')
53
50
54 @LoginRequired()
51 @LoginRequired()
55 @HasPermissionAllDecorator('hg.admin')
52 @HasPermissionAnyDecorator('hg.admin')
56 def __before__(self):
53 def _before(self, *args, **kwargs):
57 super(DefaultsController, self).__before__()
54 super(DefaultsController, self)._before(*args, **kwargs)
58
55
59 def index(self, format='html'):
56 def index(self, format='html'):
60 """GET /defaults: All items in the collection"""
61 # url('defaults')
62 c.backends = BACKENDS.keys()
57 c.backends = BACKENDS.keys()
63 defaults = Setting.get_default_repo_settings()
58 defaults = Setting.get_default_repo_settings()
64
59
@@ -69,30 +64,13 b' class DefaultsController(BaseController)'
69 force_defaults=False
64 force_defaults=False
70 )
65 )
71
66
72 def create(self):
73 """POST /defaults: Create a new item"""
74 # url('defaults')
75
76 def new(self, format='html'):
77 """GET /defaults/new: Form to create a new item"""
78 # url('new_default')
79
80 def update(self, id):
67 def update(self, id):
81 """PUT /defaults/id: Update an existing item"""
82 # Forms posted to this method should contain a hidden field:
83 # <input type="hidden" name="_method" value="PUT" />
84 # Or using helpers:
85 # h.form(url('default', id=ID),
86 # method='put')
87 # url('default', id=ID)
88
89 _form = DefaultsForm()()
68 _form = DefaultsForm()()
90
69
91 try:
70 try:
92 form_result = _form.to_python(dict(request.POST))
71 form_result = _form.to_python(dict(request.POST))
93 for k, v in form_result.iteritems():
72 for k, v in form_result.iteritems():
94 setting = Setting.create_or_update(k, v)
73 setting = Setting.create_or_update(k, v)
95 Session().add(setting)
96 Session().commit()
74 Session().commit()
97 h.flash(_('Default settings updated successfully'),
75 h.flash(_('Default settings updated successfully'),
98 category='success')
76 category='success')
@@ -112,21 +90,4 b' class DefaultsController(BaseController)'
112 h.flash(_('Error occurred during update of defaults'),
90 h.flash(_('Error occurred during update of defaults'),
113 category='error')
91 category='error')
114
92
115 return redirect(url('defaults'))
93 raise HTTPFound(location=url('defaults'))
116
117 def delete(self, id):
118 """DELETE /defaults/id: Delete an existing item"""
119 # Forms posted to this method should contain a hidden field:
120 # <input type="hidden" name="_method" value="DELETE" />
121 # Or using helpers:
122 # h.form(url('default', id=ID),
123 # method='delete')
124 # url('default', id=ID)
125
126 def show(self, id, format='html'):
127 """GET /defaults/id: Show a specific item"""
128 # url('default', id=ID)
129
130 def edit(self, id, format='html'):
131 """GET /defaults/id/edit: Form to edit an existing item"""
132 # url('edit_default', id=ID)
@@ -30,21 +30,20 b' import logging'
30 import traceback
30 import traceback
31 import formencode.htmlfill
31 import formencode.htmlfill
32
32
33 from pylons import request, response, tmpl_context as c, url
33 from tg import request, response, tmpl_context as c
34 from pylons.controllers.util import redirect
34 from tg.i18n import ugettext as _
35 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden
36
36
37 from kallithea.config.routing import url
37 from kallithea.model.forms import GistForm
38 from kallithea.model.forms import GistForm
38 from kallithea.model.gist import GistModel
39 from kallithea.model.gist import GistModel
39 from kallithea.model.meta import Session
40 from kallithea.model.meta import Session
40 from kallithea.model.db import Gist, User
41 from kallithea.model.db import Gist, User
41 from kallithea.lib import helpers as h
42 from kallithea.lib import helpers as h
42 from kallithea.lib.base import BaseController, render
43 from kallithea.lib.base import BaseController, render, jsonify
43 from kallithea.lib.auth import LoginRequired, NotAnonymous
44 from kallithea.lib.auth import LoginRequired
44 from kallithea.lib.utils import jsonify
45 from kallithea.lib.utils2 import safe_int, safe_unicode, time_to_datetime
45 from kallithea.lib.utils2 import safe_int, safe_unicode, time_to_datetime
46 from kallithea.lib.helpers import Page
46 from kallithea.lib.page import Page
47 from webob.exc import HTTPNotFound, HTTPForbidden
48 from sqlalchemy.sql.expression import or_
47 from sqlalchemy.sql.expression import or_
49 from kallithea.lib.vcs.exceptions import VCSError, NodeNotChangedError
48 from kallithea.lib.vcs.exceptions import VCSError, NodeNotChangedError
50
49
@@ -66,52 +65,47 b' class GistsController(BaseController):'
66 c.lifetime_values.append(extra_values)
65 c.lifetime_values.append(extra_values)
67 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
66 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
68
67
69 @LoginRequired()
68 @LoginRequired(allow_default_user=True)
70 def index(self):
69 def index(self):
71 """GET /admin/gists: All items in the collection"""
70 not_default_user = not request.authuser.is_default_user
72 # url('gists')
73 not_default_user = c.authuser.username != User.DEFAULT_USER
74 c.show_private = request.GET.get('private') and not_default_user
71 c.show_private = request.GET.get('private') and not_default_user
75 c.show_public = request.GET.get('public') and not_default_user
72 c.show_public = request.GET.get('public') and not_default_user
76
73
77 gists = Gist().query()\
74 gists = Gist().query() \
78 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
75 .filter_by(is_expired=False) \
79 .order_by(Gist.created_on.desc())
76 .order_by(Gist.created_on.desc())
80
77
81 # MY private
78 # MY private
82 if c.show_private and not c.show_public:
79 if c.show_private and not c.show_public:
83 gists = gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
80 gists = gists.filter(Gist.gist_type == Gist.GIST_PRIVATE) \
84 .filter(Gist.gist_owner == c.authuser.user_id)
81 .filter(Gist.owner_id == request.authuser.user_id)
85 # MY public
82 # MY public
86 elif c.show_public and not c.show_private:
83 elif c.show_public and not c.show_private:
87 gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
84 gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC) \
88 .filter(Gist.gist_owner == c.authuser.user_id)
85 .filter(Gist.owner_id == request.authuser.user_id)
89
86
90 # MY public+private
87 # MY public+private
91 elif c.show_private and c.show_public:
88 elif c.show_private and c.show_public:
92 gists = gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
89 gists = gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
93 Gist.gist_type == Gist.GIST_PRIVATE))\
90 Gist.gist_type == Gist.GIST_PRIVATE)) \
94 .filter(Gist.gist_owner == c.authuser.user_id)
91 .filter(Gist.owner_id == request.authuser.user_id)
95
92
96 # default show ALL public gists
93 # default show ALL public gists
97 if not c.show_public and not c.show_private:
94 if not c.show_public and not c.show_private:
98 gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
95 gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
99
96
100 c.gists = gists
97 c.gists = gists
101 p = safe_int(request.GET.get('page', 1), 1)
98 p = safe_int(request.GET.get('page'), 1)
102 c.gists_pager = Page(c.gists, page=p, items_per_page=10)
99 c.gists_pager = Page(c.gists, page=p, items_per_page=10)
103 return render('admin/gists/index.html')
100 return render('admin/gists/index.html')
104
101
105 @LoginRequired()
102 @LoginRequired()
106 @NotAnonymous()
107 def create(self):
103 def create(self):
108 """POST /admin/gists: Create a new item"""
109 # url('gists')
110 self.__load_defaults()
104 self.__load_defaults()
111 gist_form = GistForm([x[0] for x in c.lifetime_values])()
105 gist_form = GistForm([x[0] for x in c.lifetime_values])()
112 try:
106 try:
113 form_result = gist_form.to_python(dict(request.POST))
107 form_result = gist_form.to_python(dict(request.POST))
114 #TODO: multiple files support, from the form
108 # TODO: multiple files support, from the form
115 filename = form_result['filename'] or Gist.DEFAULT_FILENAME
109 filename = form_result['filename'] or Gist.DEFAULT_FILENAME
116 nodes = {
110 nodes = {
117 filename: {
111 filename: {
@@ -123,7 +117,7 b' class GistsController(BaseController):'
123 gist_type = Gist.GIST_PUBLIC if _public else Gist.GIST_PRIVATE
117 gist_type = Gist.GIST_PUBLIC if _public else Gist.GIST_PRIVATE
124 gist = GistModel().create(
118 gist = GistModel().create(
125 description=form_result['description'],
119 description=form_result['description'],
126 owner=c.authuser.user_id,
120 owner=request.authuser.user_id,
127 gist_mapping=nodes,
121 gist_mapping=nodes,
128 gist_type=gist_type,
122 gist_type=gist_type,
129 lifetime=form_result['lifetime']
123 lifetime=form_result['lifetime']
@@ -144,40 +138,18 b' class GistsController(BaseController):'
144 except Exception as e:
138 except Exception as e:
145 log.error(traceback.format_exc())
139 log.error(traceback.format_exc())
146 h.flash(_('Error occurred during gist creation'), category='error')
140 h.flash(_('Error occurred during gist creation'), category='error')
147 return redirect(url('new_gist'))
141 raise HTTPFound(location=url('new_gist'))
148 return redirect(url('gist', gist_id=new_gist_id))
142 raise HTTPFound(location=url('gist', gist_id=new_gist_id))
149
143
150 @LoginRequired()
144 @LoginRequired()
151 @NotAnonymous()
152 def new(self, format='html'):
145 def new(self, format='html'):
153 """GET /admin/gists/new: Form to create a new item"""
154 # url('new_gist')
155 self.__load_defaults()
146 self.__load_defaults()
156 return render('admin/gists/new.html')
147 return render('admin/gists/new.html')
157
148
158 @LoginRequired()
149 @LoginRequired()
159 @NotAnonymous()
160 def update(self, gist_id):
161 """PUT /admin/gists/gist_id: Update an existing item"""
162 # Forms posted to this method should contain a hidden field:
163 # <input type="hidden" name="_method" value="PUT" />
164 # Or using helpers:
165 # h.form(url('gist', gist_id=ID),
166 # method='put')
167 # url('gist', gist_id=ID)
168
169 @LoginRequired()
170 @NotAnonymous()
171 def delete(self, gist_id):
150 def delete(self, gist_id):
172 """DELETE /admin/gists/gist_id: Delete an existing item"""
173 # Forms posted to this method should contain a hidden field:
174 # <input type="hidden" name="_method" value="DELETE" />
175 # Or using helpers:
176 # h.form(url('gist', gist_id=ID),
177 # method='delete')
178 # url('gist', gist_id=ID)
179 gist = GistModel().get_gist(gist_id)
151 gist = GistModel().get_gist(gist_id)
180 owner = gist.gist_owner == c.authuser.user_id
152 owner = gist.owner_id == request.authuser.user_id
181 if h.HasPermissionAny('hg.admin')() or owner:
153 if h.HasPermissionAny('hg.admin')() or owner:
182 GistModel().delete(gist)
154 GistModel().delete(gist)
183 Session().commit()
155 Session().commit()
@@ -185,20 +157,16 b' class GistsController(BaseController):'
185 else:
157 else:
186 raise HTTPForbidden()
158 raise HTTPForbidden()
187
159
188 return redirect(url('gists'))
160 raise HTTPFound(location=url('gists'))
189
161
190 @LoginRequired()
162 @LoginRequired(allow_default_user=True)
191 def show(self, gist_id, revision='tip', format='html', f_path=None):
163 def show(self, gist_id, revision='tip', format='html', f_path=None):
192 """GET /admin/gists/gist_id: Show a specific item"""
193 # url('gist', gist_id=ID)
194 c.gist = Gist.get_or_404(gist_id)
164 c.gist = Gist.get_or_404(gist_id)
195
165
196 #check if this gist is not expired
166 if c.gist.is_expired:
197 if c.gist.gist_expires != -1:
167 log.error('Gist expired at %s',
198 if time.time() > c.gist.gist_expires:
168 time_to_datetime(c.gist.gist_expires))
199 log.error('Gist expired at %s',
169 raise HTTPNotFound()
200 time_to_datetime(c.gist.gist_expires))
201 raise HTTPNotFound()
202 try:
170 try:
203 c.file_changeset, c.files = GistModel().get_gist_files(gist_id,
171 c.file_changeset, c.files = GistModel().get_gist_files(gist_id,
204 revision=revision)
172 revision=revision)
@@ -212,18 +180,13 b' class GistsController(BaseController):'
212 return render('admin/gists/show.html')
180 return render('admin/gists/show.html')
213
181
214 @LoginRequired()
182 @LoginRequired()
215 @NotAnonymous()
216 def edit(self, gist_id, format='html'):
183 def edit(self, gist_id, format='html'):
217 """GET /admin/gists/gist_id/edit: Form to edit an existing item"""
218 # url('edit_gist', gist_id=ID)
219 c.gist = Gist.get_or_404(gist_id)
184 c.gist = Gist.get_or_404(gist_id)
220
185
221 #check if this gist is not expired
186 if c.gist.is_expired:
222 if c.gist.gist_expires != -1:
187 log.error('Gist expired at %s',
223 if time.time() > c.gist.gist_expires:
188 time_to_datetime(c.gist.gist_expires))
224 log.error('Gist expired at %s',
189 raise HTTPNotFound()
225 time_to_datetime(c.gist.gist_expires))
226 raise HTTPNotFound()
227 try:
190 try:
228 c.file_changeset, c.files = GistModel().get_gist_files(gist_id)
191 c.file_changeset, c.files = GistModel().get_gist_files(gist_id)
229 except VCSError:
192 except VCSError:
@@ -270,12 +233,11 b' class GistsController(BaseController):'
270 h.flash(_('Error occurred during update of gist %s') % gist_id,
233 h.flash(_('Error occurred during update of gist %s') % gist_id,
271 category='error')
234 category='error')
272
235
273 return redirect(url('gist', gist_id=gist_id))
236 raise HTTPFound(location=url('gist', gist_id=gist_id))
274
237
275 return rendered
238 return rendered
276
239
277 @LoginRequired()
240 @LoginRequired()
278 @NotAnonymous()
279 @jsonify
241 @jsonify
280 def check_revision(self, gist_id):
242 def check_revision(self, gist_id):
281 c.gist = Gist.get_or_404(gist_id)
243 c.gist = Gist.get_or_404(gist_id)
@@ -283,7 +245,7 b' class GistsController(BaseController):'
283 success = True
245 success = True
284 revision = request.POST.get('revision')
246 revision = request.POST.get('revision')
285
247
286 ##TODO: maybe move this to model ?
248 # TODO: maybe move this to model ?
287 if revision != last_rev.raw_id:
249 if revision != last_rev.raw_id:
288 log.error('Last revision %s is different than submitted %s',
250 log.error('Last revision %s is different than submitted %s',
289 revision, last_rev)
251 revision, last_rev)
@@ -31,17 +31,16 b' import formencode'
31
31
32 from sqlalchemy import func
32 from sqlalchemy import func
33 from formencode import htmlfill
33 from formencode import htmlfill
34 from pylons import request, tmpl_context as c, url
34 from tg import request, tmpl_context as c
35 from pylons.controllers.util import redirect
35 from tg.i18n import ugettext as _
36 from pylons.i18n.translation import _
36 from webob.exc import HTTPFound
37
37
38 from kallithea import EXTERN_TYPE_INTERNAL
38 from kallithea.config.routing import url
39 from kallithea.lib import helpers as h
39 from kallithea.lib import helpers as h
40 from kallithea.lib import auth_modules
40 from kallithea.lib import auth_modules
41 from kallithea.lib.auth import LoginRequired, NotAnonymous, AuthUser
41 from kallithea.lib.auth import LoginRequired, AuthUser
42 from kallithea.lib.base import BaseController, render
42 from kallithea.lib.base import BaseController, render
43 from kallithea.lib.utils2 import generate_api_key, safe_int
43 from kallithea.lib.utils2 import generate_api_key, safe_int
44 from kallithea.lib.compat import json
45 from kallithea.model.db import Repository, UserEmailMap, User, UserFollowing
44 from kallithea.model.db import Repository, UserEmailMap, User, UserFollowing
46 from kallithea.model.forms import UserForm, PasswordChangeForm
45 from kallithea.model.forms import UserForm, PasswordChangeForm
47 from kallithea.model.user import UserModel
46 from kallithea.model.user import UserModel
@@ -60,48 +59,37 b' class MyAccountController(BaseController'
60 # path_prefix='/admin', name_prefix='admin_')
59 # path_prefix='/admin', name_prefix='admin_')
61
60
62 @LoginRequired()
61 @LoginRequired()
63 @NotAnonymous()
62 def _before(self, *args, **kwargs):
64 def __before__(self):
63 super(MyAccountController, self)._before(*args, **kwargs)
65 super(MyAccountController, self).__before__()
66
64
67 def __load_data(self):
65 def __load_data(self):
68 c.user = User.get(self.authuser.user_id)
66 c.user = User.get(request.authuser.user_id)
69 if c.user.username == User.DEFAULT_USER:
67 if c.user.is_default_user:
70 h.flash(_("You can't edit this user since it's"
68 h.flash(_("You can't edit this user since it's"
71 " crucial for entire application"), category='warning')
69 " crucial for entire application"), category='warning')
72 return redirect(url('users'))
70 raise HTTPFound(location=url('users'))
73 c.EXTERN_TYPE_INTERNAL = EXTERN_TYPE_INTERNAL
74
71
75 def _load_my_repos_data(self, watched=False):
72 def _load_my_repos_data(self, watched=False):
76 if watched:
73 if watched:
77 admin = False
74 admin = False
78 repos_list = [x.follows_repository for x in
75 repos_list = Session().query(Repository) \
79 Session().query(UserFollowing).filter(
76 .join(UserFollowing) \
80 UserFollowing.user_id ==
77 .filter(UserFollowing.user_id ==
81 self.authuser.user_id).all()]
78 request.authuser.user_id).all()
82 else:
79 else:
83 admin = True
80 admin = True
84 repos_list = Session().query(Repository)\
81 repos_list = Session().query(Repository) \
85 .filter(Repository.user_id ==
82 .filter(Repository.owner_id ==
86 self.authuser.user_id)\
83 request.authuser.user_id).all()
87 .order_by(func.lower(Repository.repo_name)).all()
88
84
89 repos_data = RepoModel().get_repos_as_dict(repos_list=repos_list,
85 return RepoModel().get_repos_as_dict(repos_list, admin=admin)
90 admin=admin)
91 #json used to render the grid
92 return json.dumps(repos_data)
93
86
94 def my_account(self):
87 def my_account(self):
95 """
96 GET /_admin/my_account Displays info about my account
97 """
98 # url('my_account')
99 c.active = 'profile'
88 c.active = 'profile'
100 self.__load_data()
89 self.__load_data()
101 c.perm_user = AuthUser(user_id=self.authuser.user_id)
90 c.perm_user = AuthUser(user_id=request.authuser.user_id)
102 c.ip_addr = self.ip_addr
103 managed_fields = auth_modules.get_managed_fields(c.user)
91 managed_fields = auth_modules.get_managed_fields(c.user)
104 def_user_perms = User.get_default_user().AuthUser.permissions['global']
92 def_user_perms = AuthUser(dbuser=User.get_default_user()).permissions['global']
105 if 'hg.register.none' in def_user_perms:
93 if 'hg.register.none' in def_user_perms:
106 managed_fields.extend(['username', 'firstname', 'lastname', 'email'])
94 managed_fields.extend(['username', 'firstname', 'lastname', 'email'])
107
95
@@ -111,8 +99,8 b' class MyAccountController(BaseController'
111 update = False
99 update = False
112 if request.POST:
100 if request.POST:
113 _form = UserForm(edit=True,
101 _form = UserForm(edit=True,
114 old_data={'user_id': self.authuser.user_id,
102 old_data={'user_id': request.authuser.user_id,
115 'email': self.authuser.email})()
103 'email': request.authuser.email})()
116 form_result = {}
104 form_result = {}
117 try:
105 try:
118 post_data = dict(request.POST)
106 post_data = dict(request.POST)
@@ -124,7 +112,7 b' class MyAccountController(BaseController'
124 'new_password', 'password_confirmation',
112 'new_password', 'password_confirmation',
125 ] + managed_fields
113 ] + managed_fields
126
114
127 UserModel().update(self.authuser.user_id, form_result,
115 UserModel().update(request.authuser.user_id, form_result,
128 skip_attrs=skip_attrs)
116 skip_attrs=skip_attrs)
129 h.flash(_('Your account was updated successfully'),
117 h.flash(_('Your account was updated successfully'),
130 category='success')
118 category='success')
@@ -141,10 +129,10 b' class MyAccountController(BaseController'
141 force_defaults=False)
129 force_defaults=False)
142 except Exception:
130 except Exception:
143 log.error(traceback.format_exc())
131 log.error(traceback.format_exc())
144 h.flash(_('Error occurred during update of user %s') \
132 h.flash(_('Error occurred during update of user %s')
145 % form_result.get('username'), category='error')
133 % form_result.get('username'), category='error')
146 if update:
134 if update:
147 return redirect('my_account')
135 raise HTTPFound(location='my_account')
148 return htmlfill.render(
136 return htmlfill.render(
149 render('admin/my_account/my_account.html'),
137 render('admin/my_account/my_account.html'),
150 defaults=defaults,
138 defaults=defaults,
@@ -159,10 +147,10 b' class MyAccountController(BaseController'
159 c.can_change_password = 'password' not in managed_fields
147 c.can_change_password = 'password' not in managed_fields
160
148
161 if request.POST and c.can_change_password:
149 if request.POST and c.can_change_password:
162 _form = PasswordChangeForm(self.authuser.username)()
150 _form = PasswordChangeForm(request.authuser.username)()
163 try:
151 try:
164 form_result = _form.to_python(request.POST)
152 form_result = _form.to_python(request.POST)
165 UserModel().update(self.authuser.user_id, form_result)
153 UserModel().update(request.authuser.user_id, form_result)
166 Session().commit()
154 Session().commit()
167 h.flash(_("Successfully updated password"), category='success')
155 h.flash(_("Successfully updated password"), category='success')
168 except formencode.Invalid as errors:
156 except formencode.Invalid as errors:
@@ -183,7 +171,7 b' class MyAccountController(BaseController'
183 c.active = 'repos'
171 c.active = 'repos'
184 self.__load_data()
172 self.__load_data()
185
173
186 #json used to render the grid
174 # data used to render the grid
187 c.data = self._load_my_repos_data()
175 c.data = self._load_my_repos_data()
188 return render('admin/my_account/my_account.html')
176 return render('admin/my_account/my_account.html')
189
177
@@ -191,15 +179,14 b' class MyAccountController(BaseController'
191 c.active = 'watched'
179 c.active = 'watched'
192 self.__load_data()
180 self.__load_data()
193
181
194 #json used to render the grid
182 # data used to render the grid
195 c.data = self._load_my_repos_data(watched=True)
183 c.data = self._load_my_repos_data(watched=True)
196 return render('admin/my_account/my_account.html')
184 return render('admin/my_account/my_account.html')
197
185
198 def my_account_perms(self):
186 def my_account_perms(self):
199 c.active = 'perms'
187 c.active = 'perms'
200 self.__load_data()
188 self.__load_data()
201 c.perm_user = AuthUser(user_id=self.authuser.user_id)
189 c.perm_user = AuthUser(user_id=request.authuser.user_id)
202 c.ip_addr = self.ip_addr
203
190
204 return render('admin/my_account/my_account.html')
191 return render('admin/my_account/my_account.html')
205
192
@@ -207,7 +194,7 b' class MyAccountController(BaseController'
207 c.active = 'emails'
194 c.active = 'emails'
208 self.__load_data()
195 self.__load_data()
209
196
210 c.user_email_map = UserEmailMap.query()\
197 c.user_email_map = UserEmailMap.query() \
211 .filter(UserEmailMap.user == c.user).all()
198 .filter(UserEmailMap.user == c.user).all()
212 return render('admin/my_account/my_account.html')
199 return render('admin/my_account/my_account.html')
213
200
@@ -215,7 +202,7 b' class MyAccountController(BaseController'
215 email = request.POST.get('new_email')
202 email = request.POST.get('new_email')
216
203
217 try:
204 try:
218 UserModel().add_extra_email(self.authuser.user_id, email)
205 UserModel().add_extra_email(request.authuser.user_id, email)
219 Session().commit()
206 Session().commit()
220 h.flash(_("Added email %s to user") % email, category='success')
207 h.flash(_("Added email %s to user") % email, category='success')
221 except formencode.Invalid as error:
208 except formencode.Invalid as error:
@@ -225,15 +212,15 b' class MyAccountController(BaseController'
225 log.error(traceback.format_exc())
212 log.error(traceback.format_exc())
226 h.flash(_('An error occurred during email saving'),
213 h.flash(_('An error occurred during email saving'),
227 category='error')
214 category='error')
228 return redirect(url('my_account_emails'))
215 raise HTTPFound(location=url('my_account_emails'))
229
216
230 def my_account_emails_delete(self):
217 def my_account_emails_delete(self):
231 email_id = request.POST.get('del_email_id')
218 email_id = request.POST.get('del_email_id')
232 user_model = UserModel()
219 user_model = UserModel()
233 user_model.delete_extra_email(self.authuser.user_id, email_id)
220 user_model.delete_extra_email(request.authuser.user_id, email_id)
234 Session().commit()
221 Session().commit()
235 h.flash(_("Removed email from user"), category='success')
222 h.flash(_("Removed email from user"), category='success')
236 return redirect(url('my_account_emails'))
223 raise HTTPFound(location=url('my_account_emails'))
237
224
238 def my_account_api_keys(self):
225 def my_account_api_keys(self):
239 c.active = 'api_keys'
226 c.active = 'api_keys'
@@ -247,31 +234,28 b' class MyAccountController(BaseController'
247 (str(60 * 24 * 30), _('1 month')),
234 (str(60 * 24 * 30), _('1 month')),
248 ]
235 ]
249 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
236 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
250 c.user_api_keys = ApiKeyModel().get_api_keys(self.authuser.user_id,
237 c.user_api_keys = ApiKeyModel().get_api_keys(request.authuser.user_id,
251 show_expired=show_expired)
238 show_expired=show_expired)
252 return render('admin/my_account/my_account.html')
239 return render('admin/my_account/my_account.html')
253
240
254 def my_account_api_keys_add(self):
241 def my_account_api_keys_add(self):
255 lifetime = safe_int(request.POST.get('lifetime'), -1)
242 lifetime = safe_int(request.POST.get('lifetime'), -1)
256 description = request.POST.get('description')
243 description = request.POST.get('description')
257 ApiKeyModel().create(self.authuser.user_id, description, lifetime)
244 ApiKeyModel().create(request.authuser.user_id, description, lifetime)
258 Session().commit()
245 Session().commit()
259 h.flash(_("API key successfully created"), category='success')
246 h.flash(_("API key successfully created"), category='success')
260 return redirect(url('my_account_api_keys'))
247 raise HTTPFound(location=url('my_account_api_keys'))
261
248
262 def my_account_api_keys_delete(self):
249 def my_account_api_keys_delete(self):
263 api_key = request.POST.get('del_api_key')
250 api_key = request.POST.get('del_api_key')
264 user_id = self.authuser.user_id
265 if request.POST.get('del_api_key_builtin'):
251 if request.POST.get('del_api_key_builtin'):
266 user = User.get(user_id)
252 user = User.get(request.authuser.user_id)
267 if user is not None:
253 user.api_key = generate_api_key()
268 user.api_key = generate_api_key()
254 Session().commit()
269 Session().add(user)
255 h.flash(_("API key successfully reset"), category='success')
270 Session().commit()
271 h.flash(_("API key successfully reset"), category='success')
272 elif api_key:
256 elif api_key:
273 ApiKeyModel().delete(api_key, self.authuser.user_id)
257 ApiKeyModel().delete(api_key, request.authuser.user_id)
274 Session().commit()
258 Session().commit()
275 h.flash(_("API key successfully deleted"), category='success')
259 h.flash(_("API key successfully deleted"), category='success')
276
260
277 return redirect(url('my_account_api_keys'))
261 raise HTTPFound(location=url('my_account_api_keys'))
@@ -31,12 +31,13 b' import traceback'
31 import formencode
31 import formencode
32 from formencode import htmlfill
32 from formencode import htmlfill
33
33
34 from pylons import request, tmpl_context as c, url
34 from tg import request, tmpl_context as c
35 from pylons.controllers.util import redirect
35 from tg.i18n import ugettext as _
36 from pylons.i18n.translation import _
36 from webob.exc import HTTPFound
37
37
38 from kallithea.config.routing import url
38 from kallithea.lib import helpers as h
39 from kallithea.lib import helpers as h
39 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
40 from kallithea.lib.auth import LoginRequired, HasPermissionAnyDecorator, AuthUser
40 from kallithea.lib.base import BaseController, render
41 from kallithea.lib.base import BaseController, render
41 from kallithea.model.forms import DefaultPermissionsForm
42 from kallithea.model.forms import DefaultPermissionsForm
42 from kallithea.model.permission import PermissionModel
43 from kallithea.model.permission import PermissionModel
@@ -53,9 +54,9 b' class PermissionsController(BaseControll'
53 # map.resource('permission', 'permissions')
54 # map.resource('permission', 'permissions')
54
55
55 @LoginRequired()
56 @LoginRequired()
56 @HasPermissionAllDecorator('hg.admin')
57 @HasPermissionAnyDecorator('hg.admin')
57 def __before__(self):
58 def _before(self, *args, **kwargs):
58 super(PermissionsController, self).__before__()
59 super(PermissionsController, self)._before(*args, **kwargs)
59
60
60 def __load_data(self):
61 def __load_data(self):
61 c.repo_perms_choices = [('repository.none', _('None'),),
62 c.repo_perms_choices = [('repository.none', _('None'),),
@@ -139,7 +140,7 b' class PermissionsController(BaseControll'
139 h.flash(_('Error occurred during update of permissions'),
140 h.flash(_('Error occurred during update of permissions'),
140 category='error')
141 category='error')
141
142
142 return redirect(url('admin_permissions'))
143 raise HTTPFound(location=url('admin_permissions'))
143
144
144 c.user = User.get_default_user()
145 c.user = User.get_default_user()
145 defaults = {'anonymous': c.user.active}
146 defaults = {'anonymous': c.user.active}
@@ -184,7 +185,7 b' class PermissionsController(BaseControll'
184 def permission_ips(self):
185 def permission_ips(self):
185 c.active = 'ips'
186 c.active = 'ips'
186 c.user = User.get_default_user()
187 c.user = User.get_default_user()
187 c.user_ip_map = UserIpMap.query()\
188 c.user_ip_map = UserIpMap.query() \
188 .filter(UserIpMap.user == c.user).all()
189 .filter(UserIpMap.user == c.user).all()
189
190
190 return render('admin/permissions/permissions.html')
191 return render('admin/permissions/permissions.html')
@@ -192,5 +193,5 b' class PermissionsController(BaseControll'
192 def permission_perms(self):
193 def permission_perms(self):
193 c.active = 'perms'
194 c.active = 'perms'
194 c.user = User.get_default_user()
195 c.user = User.get_default_user()
195 c.perm_user = c.user.AuthUser
196 c.perm_user = AuthUser(dbuser=c.user)
196 return render('admin/permissions/permissions.html')
197 return render('admin/permissions/permissions.html')
@@ -32,16 +32,16 b' import itertools'
32
32
33 from formencode import htmlfill
33 from formencode import htmlfill
34
34
35 from pylons import request, tmpl_context as c, url
35 from tg import request, tmpl_context as c, app_globals
36 from pylons.controllers.util import abort, redirect
36 from tg.i18n import ugettext as _, ungettext
37 from pylons.i18n.translation import _, ungettext
37 from webob.exc import HTTPFound, HTTPForbidden, HTTPNotFound, HTTPInternalServerError
38
38
39 import kallithea
39 import kallithea
40 from kallithea.config.routing import url
40 from kallithea.lib import helpers as h
41 from kallithea.lib import helpers as h
41 from kallithea.lib.compat import json
42 from kallithea.lib.auth import LoginRequired, \
42 from kallithea.lib.auth import LoginRequired, \
43 HasRepoGroupPermissionAnyDecorator, HasRepoGroupPermissionAll, \
43 HasRepoGroupPermissionLevelDecorator, HasRepoGroupPermissionLevel, \
44 HasPermissionAll
44 HasPermissionAny
45 from kallithea.lib.base import BaseController, render
45 from kallithea.lib.base import BaseController, render
46 from kallithea.model.db import RepoGroup, Repository
46 from kallithea.model.db import RepoGroup, Repository
47 from kallithea.model.scm import RepoGroupList, AvailableRepoGroupChoices
47 from kallithea.model.scm import RepoGroupList, AvailableRepoGroupChoices
@@ -49,7 +49,6 b' from kallithea.model.repo_group import R'
49 from kallithea.model.forms import RepoGroupForm, RepoGroupPermsForm
49 from kallithea.model.forms import RepoGroupForm, RepoGroupPermsForm
50 from kallithea.model.meta import Session
50 from kallithea.model.meta import Session
51 from kallithea.model.repo import RepoModel
51 from kallithea.model.repo import RepoModel
52 from webob.exc import HTTPInternalServerError, HTTPNotFound
53 from kallithea.lib.utils2 import safe_int
52 from kallithea.lib.utils2 import safe_int
54 from sqlalchemy.sql.expression import func
53 from sqlalchemy.sql.expression import func
55
54
@@ -59,24 +58,20 b' log = logging.getLogger(__name__)'
59
58
60 class RepoGroupsController(BaseController):
59 class RepoGroupsController(BaseController):
61
60
62 @LoginRequired()
61 @LoginRequired(allow_default_user=True)
63 def __before__(self):
62 def _before(self, *args, **kwargs):
64 super(RepoGroupsController, self).__before__()
63 super(RepoGroupsController, self)._before(*args, **kwargs)
65
64
66 def __load_defaults(self, extras=(), exclude=()):
65 def __load_defaults(self, extras=(), exclude=()):
67 """extras is used for keeping current parent ignoring permissions
66 """extras is used for keeping current parent ignoring permissions
68 exclude is used for not moving group to itself TODO: also exclude descendants
67 exclude is used for not moving group to itself TODO: also exclude descendants
69 Note: only admin can create top level groups
68 Note: only admin can create top level groups
70 """
69 """
71 repo_groups = AvailableRepoGroupChoices([], ['group.admin'], extras)
70 repo_groups = AvailableRepoGroupChoices([], 'admin', extras)
72 exclude_group_ids = set(rg.group_id for rg in exclude)
71 exclude_group_ids = set(rg.group_id for rg in exclude)
73 c.repo_groups = [rg for rg in repo_groups
72 c.repo_groups = [rg for rg in repo_groups
74 if rg[0] not in exclude_group_ids]
73 if rg[0] not in exclude_group_ids]
75
74
76 repo_model = RepoModel()
77 c.users_array = repo_model.get_users_js()
78 c.user_groups_array = repo_model.get_user_groups_js()
79
80 def __load_data(self, group_id):
75 def __load_data(self, group_id):
81 """
76 """
82 Load defaults settings for edit, and update
77 Load defaults settings for edit, and update
@@ -100,24 +95,20 b' class RepoGroupsController(BaseControlle'
100 return data
95 return data
101
96
102 def _revoke_perms_on_yourself(self, form_result):
97 def _revoke_perms_on_yourself(self, form_result):
103 _up = filter(lambda u: c.authuser.username == u[0],
98 _up = filter(lambda u: request.authuser.username == u[0],
104 form_result['perms_updates'])
99 form_result['perms_updates'])
105 _new = filter(lambda u: c.authuser.username == u[0],
100 _new = filter(lambda u: request.authuser.username == u[0],
106 form_result['perms_new'])
101 form_result['perms_new'])
107 if _new and _new[0][1] != 'group.admin' or _up and _up[0][1] != 'group.admin':
102 if _new and _new[0][1] != 'group.admin' or _up and _up[0][1] != 'group.admin':
108 return True
103 return True
109 return False
104 return False
110
105
111 def index(self, format='html'):
106 def index(self, format='html'):
112 """GET /repo_groups: All items in the collection"""
107 _list = RepoGroup.query(sorted=True).all()
113 # url('repos_groups')
108 group_iter = RepoGroupList(_list, perm_level='admin')
114 _list = RepoGroup.query()\
115 .order_by(func.lower(RepoGroup.group_name))\
116 .all()
117 group_iter = RepoGroupList(_list, perm_set=['group.admin'])
118 repo_groups_data = []
109 repo_groups_data = []
119 total_records = len(group_iter)
110 total_records = len(group_iter)
120 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
111 _tmpl_lookup = app_globals.mako_lookup
121 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
112 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
122
113
123 repo_group_name = lambda repo_group_name, children_groups: (
114 repo_group_name = lambda repo_group_name, children_groups: (
@@ -140,25 +131,20 b' class RepoGroupsController(BaseControlle'
140 "group_name": repo_group_name(repo_gr.group_name, children_groups),
131 "group_name": repo_group_name(repo_gr.group_name, children_groups),
141 "desc": h.escape(repo_gr.group_description),
132 "desc": h.escape(repo_gr.group_description),
142 "repos": repo_count,
133 "repos": repo_count,
143 "owner": h.person(repo_gr.user),
134 "owner": h.person(repo_gr.owner),
144 "action": repo_group_actions(repo_gr.group_id, repo_gr.group_name,
135 "action": repo_group_actions(repo_gr.group_id, repo_gr.group_name,
145 repo_count)
136 repo_count)
146 })
137 })
147
138
148 c.data = json.dumps({
139 c.data = {
149 "totalRecords": total_records,
150 "startIndex": 0,
151 "sort": None,
140 "sort": None,
152 "dir": "asc",
141 "dir": "asc",
153 "records": repo_groups_data
142 "records": repo_groups_data
154 })
143 }
155
144
156 return render('admin/repo_groups/repo_groups.html')
145 return render('admin/repo_groups/repo_groups.html')
157
146
158 def create(self):
147 def create(self):
159 """POST /repo_groups: Create a new item"""
160 # url('repos_groups')
161
162 self.__load_defaults()
148 self.__load_defaults()
163
149
164 # permissions for can create group based on parent_id are checked
150 # permissions for can create group based on parent_id are checked
@@ -169,12 +155,12 b' class RepoGroupsController(BaseControlle'
169 gr = RepoGroupModel().create(
155 gr = RepoGroupModel().create(
170 group_name=form_result['group_name'],
156 group_name=form_result['group_name'],
171 group_description=form_result['group_description'],
157 group_description=form_result['group_description'],
172 parent=form_result['group_parent_id'],
158 parent=form_result['parent_group_id'],
173 owner=self.authuser.user_id, # TODO: make editable
159 owner=request.authuser.user_id, # TODO: make editable
174 copy_permissions=form_result['group_copy_permissions']
160 copy_permissions=form_result['group_copy_permissions']
175 )
161 )
176 Session().commit()
162 Session().commit()
177 #TODO: in futureaction_logger(, '', '', '', self.sa)
163 # TODO: in future action_logger(, '', '', '')
178 except formencode.Invalid as errors:
164 except formencode.Invalid as errors:
179 return htmlfill.render(
165 return htmlfill.render(
180 render('admin/repo_groups/repo_group_add.html'),
166 render('admin/repo_groups/repo_group_add.html'),
@@ -185,20 +171,18 b' class RepoGroupsController(BaseControlle'
185 force_defaults=False)
171 force_defaults=False)
186 except Exception:
172 except Exception:
187 log.error(traceback.format_exc())
173 log.error(traceback.format_exc())
188 h.flash(_('Error occurred during creation of repository group %s') \
174 h.flash(_('Error occurred during creation of repository group %s')
189 % request.POST.get('group_name'), category='error')
175 % request.POST.get('group_name'), category='error')
190 parent_group_id = form_result['group_parent_id']
176 parent_group_id = form_result['parent_group_id']
191 #TODO: maybe we should get back to the main view, not the admin one
177 # TODO: maybe we should get back to the main view, not the admin one
192 return redirect(url('repos_groups', parent_group=parent_group_id))
178 raise HTTPFound(location=url('repos_groups', parent_group=parent_group_id))
193 h.flash(_('Created repository group %s') % gr.group_name,
179 h.flash(_('Created repository group %s') % gr.group_name,
194 category='success')
180 category='success')
195 return redirect(url('repos_group_home', group_name=gr.group_name))
181 raise HTTPFound(location=url('repos_group_home', group_name=gr.group_name))
196
182
197 def new(self):
183 def new(self):
198 """GET /repo_groups/new: Form to create a new item"""
184 if HasPermissionAny('hg.admin')('group create'):
199 # url('new_repos_group')
185 # we're global admin, we're ok and we can create TOP level groups
200 if HasPermissionAll('hg.admin')('group create'):
201 #we're global admin, we're ok and we can create TOP level groups
202 pass
186 pass
203 else:
187 else:
204 # we pass in parent group into creation form, thus we know
188 # we pass in parent group into creation form, thus we know
@@ -206,31 +190,23 b' class RepoGroupsController(BaseControlle'
206 group_id = safe_int(request.GET.get('parent_group'))
190 group_id = safe_int(request.GET.get('parent_group'))
207 group = RepoGroup.get(group_id) if group_id else None
191 group = RepoGroup.get(group_id) if group_id else None
208 group_name = group.group_name if group else None
192 group_name = group.group_name if group else None
209 if HasRepoGroupPermissionAll('group.admin')(group_name, 'group create'):
193 if HasRepoGroupPermissionLevel('admin')(group_name, 'group create'):
210 pass
194 pass
211 else:
195 else:
212 return abort(403)
196 raise HTTPForbidden()
213
197
214 self.__load_defaults()
198 self.__load_defaults()
215 return render('admin/repo_groups/repo_group_add.html')
199 return render('admin/repo_groups/repo_group_add.html')
216
200
217 @HasRepoGroupPermissionAnyDecorator('group.admin')
201 @HasRepoGroupPermissionLevelDecorator('admin')
218 def update(self, group_name):
202 def update(self, group_name):
219 """PUT /repo_groups/group_name: Update an existing item"""
203 c.repo_group = RepoGroup.guess_instance(group_name)
220 # Forms posted to this method should contain a hidden field:
221 # <input type="hidden" name="_method" value="PUT" />
222 # Or using helpers:
223 # h.form(url('repos_group', group_name=GROUP_NAME),
224 # method='put')
225 # url('repos_group', group_name=GROUP_NAME)
226
227 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
228 self.__load_defaults(extras=[c.repo_group.parent_group],
204 self.__load_defaults(extras=[c.repo_group.parent_group],
229 exclude=[c.repo_group])
205 exclude=[c.repo_group])
230
206
231 # TODO: kill allow_empty_group - it is only used for redundant form validation!
207 # TODO: kill allow_empty_group - it is only used for redundant form validation!
232 if HasPermissionAll('hg.admin')('group edit'):
208 if HasPermissionAny('hg.admin')('group edit'):
233 #we're global admin, we're ok and we can create TOP level groups
209 # we're global admin, we're ok and we can create TOP level groups
234 allow_empty_group = True
210 allow_empty_group = True
235 elif not c.repo_group.parent_group:
211 elif not c.repo_group.parent_group:
236 allow_empty_group = True
212 allow_empty_group = True
@@ -247,13 +223,13 b' class RepoGroupsController(BaseControlle'
247
223
248 new_gr = RepoGroupModel().update(group_name, form_result)
224 new_gr = RepoGroupModel().update(group_name, form_result)
249 Session().commit()
225 Session().commit()
250 h.flash(_('Updated repository group %s') \
226 h.flash(_('Updated repository group %s')
251 % form_result['group_name'], category='success')
227 % form_result['group_name'], category='success')
252 # we now have new name !
228 # we now have new name !
253 group_name = new_gr.group_name
229 group_name = new_gr.group_name
254 #TODO: in future action_logger(, '', '', '', self.sa)
230 # TODO: in future action_logger(, '', '', '')
255 except formencode.Invalid as errors:
231 except formencode.Invalid as errors:
256
232 c.active = 'settings'
257 return htmlfill.render(
233 return htmlfill.render(
258 render('admin/repo_groups/repo_group_edit.html'),
234 render('admin/repo_groups/repo_group_edit.html'),
259 defaults=errors.value,
235 defaults=errors.value,
@@ -263,48 +239,40 b' class RepoGroupsController(BaseControlle'
263 force_defaults=False)
239 force_defaults=False)
264 except Exception:
240 except Exception:
265 log.error(traceback.format_exc())
241 log.error(traceback.format_exc())
266 h.flash(_('Error occurred during update of repository group %s') \
242 h.flash(_('Error occurred during update of repository group %s')
267 % request.POST.get('group_name'), category='error')
243 % request.POST.get('group_name'), category='error')
268
244
269 return redirect(url('edit_repo_group', group_name=group_name))
245 raise HTTPFound(location=url('edit_repo_group', group_name=group_name))
270
246
271 @HasRepoGroupPermissionAnyDecorator('group.admin')
247 @HasRepoGroupPermissionLevelDecorator('admin')
272 def delete(self, group_name):
248 def delete(self, group_name):
273 """DELETE /repo_groups/group_name: Delete an existing item"""
249 gr = c.repo_group = RepoGroup.guess_instance(group_name)
274 # Forms posted to this method should contain a hidden field:
275 # <input type="hidden" name="_method" value="DELETE" />
276 # Or using helpers:
277 # h.form(url('repos_group', group_name=GROUP_NAME),
278 # method='delete')
279 # url('repos_group', group_name=GROUP_NAME)
280
281 gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
282 repos = gr.repositories.all()
250 repos = gr.repositories.all()
283 if repos:
251 if repos:
284 h.flash(_('This group contains %s repositories and cannot be '
252 h.flash(_('This group contains %s repositories and cannot be '
285 'deleted') % len(repos), category='warning')
253 'deleted') % len(repos), category='warning')
286 return redirect(url('repos_groups'))
254 raise HTTPFound(location=url('repos_groups'))
287
255
288 children = gr.children.all()
256 children = gr.children.all()
289 if children:
257 if children:
290 h.flash(_('This group contains %s subgroups and cannot be deleted'
258 h.flash(_('This group contains %s subgroups and cannot be deleted'
291 % (len(children))), category='warning')
259 % (len(children))), category='warning')
292 return redirect(url('repos_groups'))
260 raise HTTPFound(location=url('repos_groups'))
293
261
294 try:
262 try:
295 RepoGroupModel().delete(group_name)
263 RepoGroupModel().delete(group_name)
296 Session().commit()
264 Session().commit()
297 h.flash(_('Removed repository group %s') % group_name,
265 h.flash(_('Removed repository group %s') % group_name,
298 category='success')
266 category='success')
299 #TODO: in future action_logger(, '', '', '', self.sa)
267 # TODO: in future action_logger(, '', '', '')
300 except Exception:
268 except Exception:
301 log.error(traceback.format_exc())
269 log.error(traceback.format_exc())
302 h.flash(_('Error occurred during deletion of repository group %s')
270 h.flash(_('Error occurred during deletion of repository group %s')
303 % group_name, category='error')
271 % group_name, category='error')
304
272
305 if gr.parent_group:
273 if gr.parent_group:
306 return redirect(url('repos_group_home', group_name=gr.parent_group.group_name))
274 raise HTTPFound(location=url('repos_group_home', group_name=gr.parent_group.group_name))
307 return redirect(url('repos_groups'))
275 raise HTTPFound(location=url('repos_groups'))
308
276
309 def show_by_name(self, group_name):
277 def show_by_name(self, group_name):
310 """
278 """
@@ -317,42 +285,27 b' class RepoGroupsController(BaseControlle'
317 return self.show(group_name)
285 return self.show(group_name)
318 raise HTTPNotFound
286 raise HTTPNotFound
319
287
320 @HasRepoGroupPermissionAnyDecorator('group.read', 'group.write',
288 @HasRepoGroupPermissionLevelDecorator('read')
321 'group.admin')
322 def show(self, group_name):
289 def show(self, group_name):
323 """GET /repo_groups/group_name: Show a specific item"""
324 # url('repos_group', group_name=GROUP_NAME)
325 c.active = 'settings'
290 c.active = 'settings'
326
291
327 c.group = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
292 c.group = c.repo_group = RepoGroup.guess_instance(group_name)
328 c.group_repos = c.group.repositories.all()
329
293
330 #overwrite our cached list with current filter
294 groups = RepoGroup.query(sorted=True).filter_by(parent_group=c.group).all()
331 c.repo_cnt = 0
295 repo_groups_list = self.scm_model.get_repo_groups(groups)
332
333 groups = RepoGroup.query().order_by(RepoGroup.group_name)\
334 .filter(RepoGroup.group_parent_id == c.group.group_id).all()
335 c.groups = self.scm_model.get_repo_groups(groups)
336
296
337 c.repos_list = Repository.query()\
297 repos_list = Repository.query(sorted=True).filter_by(group=c.group).all()
338 .filter(Repository.group_id == c.group.group_id)\
298 c.data = RepoModel().get_repos_as_dict(repos_list,
339 .order_by(func.lower(Repository.repo_name))\
299 repo_groups_list=repo_groups_list,
340 .all()
300 short_name=True)
341
342 repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
343 admin=False)
344 #json used to render the grid
345 c.data = json.dumps(repos_data)
346
301
347 return render('admin/repo_groups/repo_group_show.html')
302 return render('admin/repo_groups/repo_group_show.html')
348
303
349 @HasRepoGroupPermissionAnyDecorator('group.admin')
304 @HasRepoGroupPermissionLevelDecorator('admin')
350 def edit(self, group_name):
305 def edit(self, group_name):
351 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
352 # url('edit_repo_group', group_name=GROUP_NAME)
353 c.active = 'settings'
306 c.active = 'settings'
354
307
355 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
308 c.repo_group = RepoGroup.guess_instance(group_name)
356 self.__load_defaults(extras=[c.repo_group.parent_group],
309 self.__load_defaults(extras=[c.repo_group.parent_group],
357 exclude=[c.repo_group])
310 exclude=[c.repo_group])
358 defaults = self.__load_data(c.repo_group.group_id)
311 defaults = self.__load_data(c.repo_group.group_id)
@@ -364,21 +317,17 b' class RepoGroupsController(BaseControlle'
364 force_defaults=False
317 force_defaults=False
365 )
318 )
366
319
367 @HasRepoGroupPermissionAnyDecorator('group.admin')
320 @HasRepoGroupPermissionLevelDecorator('admin')
368 def edit_repo_group_advanced(self, group_name):
321 def edit_repo_group_advanced(self, group_name):
369 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
370 # url('edit_repo_group', group_name=GROUP_NAME)
371 c.active = 'advanced'
322 c.active = 'advanced'
372 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
323 c.repo_group = RepoGroup.guess_instance(group_name)
373
324
374 return render('admin/repo_groups/repo_group_edit.html')
325 return render('admin/repo_groups/repo_group_edit.html')
375
326
376 @HasRepoGroupPermissionAnyDecorator('group.admin')
327 @HasRepoGroupPermissionLevelDecorator('admin')
377 def edit_repo_group_perms(self, group_name):
328 def edit_repo_group_perms(self, group_name):
378 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
379 # url('edit_repo_group', group_name=GROUP_NAME)
380 c.active = 'perms'
329 c.active = 'perms'
381 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
330 c.repo_group = RepoGroup.guess_instance(group_name)
382 self.__load_defaults()
331 self.__load_defaults()
383 defaults = self.__load_data(c.repo_group.group_id)
332 defaults = self.__load_data(c.repo_group.group_id)
384
333
@@ -389,7 +338,7 b' class RepoGroupsController(BaseControlle'
389 force_defaults=False
338 force_defaults=False
390 )
339 )
391
340
392 @HasRepoGroupPermissionAnyDecorator('group.admin')
341 @HasRepoGroupPermissionLevelDecorator('admin')
393 def update_perms(self, group_name):
342 def update_perms(self, group_name):
394 """
343 """
395 Update permissions for given repository group
344 Update permissions for given repository group
@@ -397,14 +346,14 b' class RepoGroupsController(BaseControlle'
397 :param group_name:
346 :param group_name:
398 """
347 """
399
348
400 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
349 c.repo_group = RepoGroup.guess_instance(group_name)
401 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
350 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
402 form_result = RepoGroupPermsForm(valid_recursive_choices)().to_python(request.POST)
351 form_result = RepoGroupPermsForm(valid_recursive_choices)().to_python(request.POST)
403 if not c.authuser.is_admin:
352 if not request.authuser.is_admin:
404 if self._revoke_perms_on_yourself(form_result):
353 if self._revoke_perms_on_yourself(form_result):
405 msg = _('Cannot revoke permission for yourself as admin')
354 msg = _('Cannot revoke permission for yourself as admin')
406 h.flash(msg, category='warning')
355 h.flash(msg, category='warning')
407 return redirect(url('edit_repo_group_perms', group_name=group_name))
356 raise HTTPFound(location=url('edit_repo_group_perms', group_name=group_name))
408 recursive = form_result['recursive']
357 recursive = form_result['recursive']
409 # iterate over all members(if in recursive mode) of this groups and
358 # iterate over all members(if in recursive mode) of this groups and
410 # set the permissions !
359 # set the permissions !
@@ -413,20 +362,15 b' class RepoGroupsController(BaseControlle'
413 form_result['perms_new'],
362 form_result['perms_new'],
414 form_result['perms_updates'],
363 form_result['perms_updates'],
415 recursive)
364 recursive)
416 #TODO: implement this
365 # TODO: implement this
417 #action_logger(self.authuser, 'admin_changed_repo_permissions',
366 #action_logger(request.authuser, 'admin_changed_repo_permissions',
418 # repo_name, self.ip_addr, self.sa)
367 # repo_name, request.ip_addr)
419 Session().commit()
368 Session().commit()
420 h.flash(_('Repository group permissions updated'), category='success')
369 h.flash(_('Repository group permissions updated'), category='success')
421 return redirect(url('edit_repo_group_perms', group_name=group_name))
370 raise HTTPFound(location=url('edit_repo_group_perms', group_name=group_name))
422
371
423 @HasRepoGroupPermissionAnyDecorator('group.admin')
372 @HasRepoGroupPermissionLevelDecorator('admin')
424 def delete_perms(self, group_name):
373 def delete_perms(self, group_name):
425 """
426 DELETE an existing repository group permission user
427
428 :param group_name:
429 """
430 try:
374 try:
431 obj_type = request.POST.get('obj_type')
375 obj_type = request.POST.get('obj_type')
432 obj_id = None
376 obj_id = None
@@ -435,8 +379,8 b' class RepoGroupsController(BaseControlle'
435 elif obj_type == 'user_group':
379 elif obj_type == 'user_group':
436 obj_id = safe_int(request.POST.get('user_group_id'))
380 obj_id = safe_int(request.POST.get('user_group_id'))
437
381
438 if not c.authuser.is_admin:
382 if not request.authuser.is_admin:
439 if obj_type == 'user' and c.authuser.user_id == obj_id:
383 if obj_type == 'user' and request.authuser.user_id == obj_id:
440 msg = _('Cannot revoke permission for yourself as admin')
384 msg = _('Cannot revoke permission for yourself as admin')
441 h.flash(msg, category='warning')
385 h.flash(msg, category='warning')
442 raise Exception('revoke admin permission on self')
386 raise Exception('revoke admin permission on self')
@@ -29,26 +29,24 b' import logging'
29 import traceback
29 import traceback
30 import formencode
30 import formencode
31 from formencode import htmlfill
31 from formencode import htmlfill
32 from webob.exc import HTTPInternalServerError, HTTPForbidden, HTTPNotFound
32 from tg import request, tmpl_context as c
33 from pylons import request, tmpl_context as c, url
33 from tg.i18n import ugettext as _
34 from pylons.controllers.util import redirect
35 from pylons.i18n.translation import _
36 from sqlalchemy.sql.expression import func
34 from sqlalchemy.sql.expression import func
35 from webob.exc import HTTPFound, HTTPInternalServerError, HTTPForbidden, HTTPNotFound
37
36
37 from kallithea.config.routing import url
38 from kallithea.lib import helpers as h
38 from kallithea.lib import helpers as h
39 from kallithea.lib.auth import LoginRequired, \
39 from kallithea.lib.auth import LoginRequired, \
40 HasRepoPermissionAllDecorator, NotAnonymous, HasPermissionAny, \
40 HasRepoPermissionLevelDecorator, NotAnonymous, HasPermissionAny
41 HasRepoPermissionAnyDecorator
41 from kallithea.lib.base import BaseRepoController, render, jsonify
42 from kallithea.lib.base import BaseRepoController, render
42 from kallithea.lib.utils import action_logger
43 from kallithea.lib.utils import action_logger, jsonify
44 from kallithea.lib.vcs import RepositoryError
43 from kallithea.lib.vcs import RepositoryError
45 from kallithea.model.meta import Session
44 from kallithea.model.meta import Session
46 from kallithea.model.db import User, Repository, UserFollowing, RepoGroup,\
45 from kallithea.model.db import User, Repository, UserFollowing, RepoGroup, \
47 Setting, RepositoryField
46 Setting, RepositoryField
48 from kallithea.model.forms import RepoForm, RepoFieldForm, RepoPermsForm
47 from kallithea.model.forms import RepoForm, RepoFieldForm, RepoPermsForm
49 from kallithea.model.scm import ScmModel, AvailableRepoGroupChoices, RepoList
48 from kallithea.model.scm import ScmModel, AvailableRepoGroupChoices, RepoList
50 from kallithea.model.repo import RepoModel
49 from kallithea.model.repo import RepoModel
51 from kallithea.lib.compat import json
52 from kallithea.lib.exceptions import AttachedForksError
50 from kallithea.lib.exceptions import AttachedForksError
53 from kallithea.lib.utils2 import safe_int
51 from kallithea.lib.utils2 import safe_int
54
52
@@ -62,81 +60,68 b' class ReposController(BaseRepoController'
62 # file has a resource setup:
60 # file has a resource setup:
63 # map.resource('repo', 'repos')
61 # map.resource('repo', 'repos')
64
62
65 @LoginRequired()
63 @LoginRequired(allow_default_user=True)
66 def __before__(self):
64 def _before(self, *args, **kwargs):
67 super(ReposController, self).__before__()
65 super(ReposController, self)._before(*args, **kwargs)
68
66
69 def _load_repo(self, repo_name):
67 def _load_repo(self):
70 repo_obj = Repository.get_by_repo_name(repo_name)
68 repo_obj = c.db_repo
71
69
72 if repo_obj is None:
70 if repo_obj is None:
73 h.not_mapped_error(repo_name)
71 h.not_mapped_error(c.repo_name)
74 return redirect(url('repos'))
72 raise HTTPFound(location=url('repos'))
75
73
76 return repo_obj
74 return repo_obj
77
75
78 def __load_defaults(self, repo=None):
76 def __load_defaults(self, repo=None):
79 top_perms = ['hg.create.repository']
77 top_perms = ['hg.create.repository']
80 repo_group_perms = ['group.admin']
81 if HasPermissionAny('hg.create.write_on_repogroup.true')():
78 if HasPermissionAny('hg.create.write_on_repogroup.true')():
82 repo_group_perms.append('group.write')
79 repo_group_perm_level = 'write'
80 else:
81 repo_group_perm_level = 'admin'
83 extras = [] if repo is None else [repo.group]
82 extras = [] if repo is None else [repo.group]
84
83
85 c.repo_groups = AvailableRepoGroupChoices(top_perms, repo_group_perms, extras)
84 c.repo_groups = AvailableRepoGroupChoices(top_perms, repo_group_perm_level, extras)
86
85
87 c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs(repo)
86 c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs(repo)
88
87
89 def __load_data(self, repo_name=None):
88 def __load_data(self):
90 """
89 """
91 Load defaults settings for edit, and update
90 Load defaults settings for edit, and update
92
93 :param repo_name:
94 """
91 """
95 c.repo_info = self._load_repo(repo_name)
92 c.repo_info = self._load_repo()
96 self.__load_defaults(c.repo_info)
93 self.__load_defaults(c.repo_info)
97
94
98 defaults = RepoModel()._get_defaults(repo_name)
95 defaults = RepoModel()._get_defaults(c.repo_name)
99 defaults['clone_uri'] = c.repo_info.clone_uri_hidden # don't show password
96 defaults['clone_uri'] = c.repo_info.clone_uri_hidden # don't show password
100
97
101 return defaults
98 return defaults
102
99
103 def index(self, format='html'):
100 def index(self, format='html'):
104 """GET /repos: All items in the collection"""
101 _list = Repository.query(sorted=True).all()
105 # url('repos')
106 _list = Repository.query()\
107 .order_by(func.lower(Repository.repo_name))\
108 .all()
109
102
110 c.repos_list = RepoList(_list, perm_set=['repository.admin'])
103 c.repos_list = RepoList(_list, perm_level='admin')
111 repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
104 # the repo list will be filtered to only show repos where the user has read permissions
112 admin=True,
105 repos_data = RepoModel().get_repos_as_dict(c.repos_list, admin=True)
113 super_user_actions=True)
106 # data used to render the grid
114 #json used to render the grid
107 c.data = repos_data
115 c.data = json.dumps(repos_data)
116
108
117 return render('admin/repos/repos.html')
109 return render('admin/repos/repos.html')
118
110
119 @NotAnonymous()
111 @NotAnonymous()
120 def create(self):
112 def create(self):
121 """
122 POST /repos: Create a new item"""
123 # url('repos')
124
125 self.__load_defaults()
113 self.__load_defaults()
126 form_result = {}
114 form_result = {}
127 task_id = None
128 try:
115 try:
129 # CanWriteGroup validators checks permissions of this POST
116 # CanWriteGroup validators checks permissions of this POST
130 form_result = RepoForm(repo_groups=c.repo_groups,
117 form_result = RepoForm(repo_groups=c.repo_groups,
131 landing_revs=c.landing_revs_choices)()\
118 landing_revs=c.landing_revs_choices)() \
132 .to_python(dict(request.POST))
119 .to_python(dict(request.POST))
133
120
134 # create is done sometimes async on celery, db transaction
121 # create is done sometimes async on celery, db transaction
135 # management is handled there.
122 # management is handled there.
136 task = RepoModel().create(form_result, self.authuser.user_id)
123 task = RepoModel().create(form_result, request.authuser.user_id)
137 from celery.result import BaseAsyncResult
124 task_id = task.task_id
138 if isinstance(task, BaseAsyncResult):
139 task_id = task.task_id
140 except formencode.Invalid as errors:
125 except formencode.Invalid as errors:
141 log.info(errors)
126 log.info(errors)
142 return htmlfill.render(
127 return htmlfill.render(
@@ -152,15 +137,14 b' class ReposController(BaseRepoController'
152 msg = (_('Error creating repository %s')
137 msg = (_('Error creating repository %s')
153 % form_result.get('repo_name'))
138 % form_result.get('repo_name'))
154 h.flash(msg, category='error')
139 h.flash(msg, category='error')
155 return redirect(url('home'))
140 raise HTTPFound(location=url('home'))
156
141
157 return redirect(h.url('repo_creating_home',
142 raise HTTPFound(location=h.url('repo_creating_home',
158 repo_name=form_result['repo_name_full'],
143 repo_name=form_result['repo_name_full'],
159 task_id=task_id))
144 task_id=task_id))
160
145
161 @NotAnonymous()
146 @NotAnonymous()
162 def create_repository(self):
147 def create_repository(self):
163 """GET /_admin/create_repository: Form to create a new item"""
164 self.__load_defaults()
148 self.__load_defaults()
165 if not c.repo_groups:
149 if not c.repo_groups:
166 raise HTTPForbidden
150 raise HTTPForbidden
@@ -184,7 +168,6 b' class ReposController(BaseRepoController'
184 force_defaults=False)
168 force_defaults=False)
185
169
186 @LoginRequired()
170 @LoginRequired()
187 @NotAnonymous()
188 def repo_creating(self, repo_name):
171 def repo_creating(self, repo_name):
189 c.repo = repo_name
172 c.repo = repo_name
190 c.task_id = request.GET.get('task_id')
173 c.task_id = request.GET.get('task_id')
@@ -193,7 +176,6 b' class ReposController(BaseRepoController'
193 return render('admin/repos/repo_creating.html')
176 return render('admin/repos/repo_creating.html')
194
177
195 @LoginRequired()
178 @LoginRequired()
196 @NotAnonymous()
197 @jsonify
179 @jsonify
198 def repo_check(self, repo_name):
180 def repo_check(self, repo_name):
199 c.repo = repo_name
181 c.repo = repo_name
@@ -201,9 +183,9 b' class ReposController(BaseRepoController'
201
183
202 if task_id and task_id not in ['None']:
184 if task_id and task_id not in ['None']:
203 from kallithea import CELERY_ON
185 from kallithea import CELERY_ON
204 from celery.result import AsyncResult
186 from kallithea.lib import celerypylons
205 if CELERY_ON:
187 if CELERY_ON:
206 task = AsyncResult(task_id)
188 task = celerypylons.result.AsyncResult(task_id)
207 if task.failed():
189 if task.failed():
208 raise HTTPInternalServerError(task.traceback)
190 raise HTTPInternalServerError(task.traceback)
209
191
@@ -227,20 +209,12 b' class ReposController(BaseRepoController'
227 return {'result': True}
209 return {'result': True}
228 return {'result': False}
210 return {'result': False}
229
211
230 @HasRepoPermissionAllDecorator('repository.admin')
212 @HasRepoPermissionLevelDecorator('admin')
231 def update(self, repo_name):
213 def update(self, repo_name):
232 """
214 c.repo_info = self._load_repo()
233 PUT /repos/repo_name: Update an existing item"""
234 # Forms posted to this method should contain a hidden field:
235 # <input type="hidden" name="_method" value="PUT" />
236 # Or using helpers:
237 # h.form(url('put_repo', repo_name=ID),
238 # method='put')
239 # url('put_repo', repo_name=ID)
240 c.repo_info = self._load_repo(repo_name)
241 self.__load_defaults(c.repo_info)
215 self.__load_defaults(c.repo_info)
242 c.active = 'settings'
216 c.active = 'settings'
243 c.repo_fields = RepositoryField.query()\
217 c.repo_fields = RepositoryField.query() \
244 .filter(RepositoryField.repository == c.repo_info).all()
218 .filter(RepositoryField.repository == c.repo_info).all()
245
219
246 repo_model = RepoModel()
220 repo_model = RepoModel()
@@ -262,14 +236,13 b' class ReposController(BaseRepoController'
262 h.flash(_('Repository %s updated successfully') % repo_name,
236 h.flash(_('Repository %s updated successfully') % repo_name,
263 category='success')
237 category='success')
264 changed_name = repo.repo_name
238 changed_name = repo.repo_name
265 action_logger(self.authuser, 'admin_updated_repo',
239 action_logger(request.authuser, 'admin_updated_repo',
266 changed_name, self.ip_addr, self.sa)
240 changed_name, request.ip_addr)
267 Session().commit()
241 Session().commit()
268 except formencode.Invalid as errors:
242 except formencode.Invalid as errors:
269 log.info(errors)
243 log.info(errors)
270 defaults = self.__load_data(repo_name)
244 defaults = self.__load_data()
271 defaults.update(errors.value)
245 defaults.update(errors.value)
272 c.users_array = repo_model.get_users_js()
273 return htmlfill.render(
246 return htmlfill.render(
274 render('admin/repos/repo_edit.html'),
247 render('admin/repos/repo_edit.html'),
275 defaults=defaults,
248 defaults=defaults,
@@ -280,26 +253,17 b' class ReposController(BaseRepoController'
280
253
281 except Exception:
254 except Exception:
282 log.error(traceback.format_exc())
255 log.error(traceback.format_exc())
283 h.flash(_('Error occurred during update of repository %s') \
256 h.flash(_('Error occurred during update of repository %s')
284 % repo_name, category='error')
257 % repo_name, category='error')
285 return redirect(url('edit_repo', repo_name=changed_name))
258 raise HTTPFound(location=url('edit_repo', repo_name=changed_name))
286
259
287 @HasRepoPermissionAllDecorator('repository.admin')
260 @HasRepoPermissionLevelDecorator('admin')
288 def delete(self, repo_name):
261 def delete(self, repo_name):
289 """
290 DELETE /repos/repo_name: Delete an existing item"""
291 # Forms posted to this method should contain a hidden field:
292 # <input type="hidden" name="_method" value="DELETE" />
293 # Or using helpers:
294 # h.form(url('delete_repo', repo_name=ID),
295 # method='delete')
296 # url('delete_repo', repo_name=ID)
297
298 repo_model = RepoModel()
262 repo_model = RepoModel()
299 repo = repo_model.get_by_repo_name(repo_name)
263 repo = repo_model.get_by_repo_name(repo_name)
300 if not repo:
264 if not repo:
301 h.not_mapped_error(repo_name)
265 h.not_mapped_error(repo_name)
302 return redirect(url('repos'))
266 raise HTTPFound(location=url('repos'))
303 try:
267 try:
304 _forks = repo.forks.count()
268 _forks = repo.forks.count()
305 handle_forks = None
269 handle_forks = None
@@ -312,8 +276,8 b' class ReposController(BaseRepoController'
312 handle_forks = 'delete'
276 handle_forks = 'delete'
313 h.flash(_('Deleted %s forks') % _forks, category='success')
277 h.flash(_('Deleted %s forks') % _forks, category='success')
314 repo_model.delete(repo, forks=handle_forks)
278 repo_model.delete(repo, forks=handle_forks)
315 action_logger(self.authuser, 'admin_deleted_repo',
279 action_logger(request.authuser, 'admin_deleted_repo',
316 repo_name, self.ip_addr, self.sa)
280 repo_name, request.ip_addr)
317 ScmModel().mark_for_invalidation(repo_name)
281 ScmModel().mark_for_invalidation(repo_name)
318 h.flash(_('Deleted repository %s') % repo_name, category='success')
282 h.flash(_('Deleted repository %s') % repo_name, category='success')
319 Session().commit()
283 Session().commit()
@@ -327,18 +291,14 b' class ReposController(BaseRepoController'
327 category='error')
291 category='error')
328
292
329 if repo.group:
293 if repo.group:
330 return redirect(url('repos_group_home', group_name=repo.group.group_name))
294 raise HTTPFound(location=url('repos_group_home', group_name=repo.group.group_name))
331 return redirect(url('repos'))
295 raise HTTPFound(location=url('repos'))
332
296
333 @HasRepoPermissionAllDecorator('repository.admin')
297 @HasRepoPermissionLevelDecorator('admin')
334 def edit(self, repo_name):
298 def edit(self, repo_name):
335 """GET /repo_name/settings: Form to edit an existing item"""
299 defaults = self.__load_data()
336 # url('edit_repo', repo_name=ID)
300 c.repo_fields = RepositoryField.query() \
337 defaults = self.__load_data(repo_name)
338 c.repo_fields = RepositoryField.query()\
339 .filter(RepositoryField.repository == c.repo_info).all()
301 .filter(RepositoryField.repository == c.repo_info).all()
340 repo_model = RepoModel()
341 c.users_array = repo_model.get_users_js()
342 c.active = 'settings'
302 c.active = 'settings'
343 return htmlfill.render(
303 return htmlfill.render(
344 render('admin/repos/repo_edit.html'),
304 render('admin/repos/repo_edit.html'),
@@ -346,14 +306,9 b' class ReposController(BaseRepoController'
346 encoding="UTF-8",
306 encoding="UTF-8",
347 force_defaults=False)
307 force_defaults=False)
348
308
349 @HasRepoPermissionAllDecorator('repository.admin')
309 @HasRepoPermissionLevelDecorator('admin')
350 def edit_permissions(self, repo_name):
310 def edit_permissions(self, repo_name):
351 """GET /repo_name/settings: Form to edit an existing item"""
311 c.repo_info = self._load_repo()
352 # url('edit_repo', repo_name=ID)
353 c.repo_info = self._load_repo(repo_name)
354 repo_model = RepoModel()
355 c.users_array = repo_model.get_users_js()
356 c.user_groups_array = repo_model.get_user_groups_js()
357 c.active = 'permissions'
312 c.active = 'permissions'
358 defaults = RepoModel()._get_defaults(repo_name)
313 defaults = RepoModel()._get_defaults(repo_name)
359
314
@@ -363,19 +318,19 b' class ReposController(BaseRepoController'
363 encoding="UTF-8",
318 encoding="UTF-8",
364 force_defaults=False)
319 force_defaults=False)
365
320
366 @HasRepoPermissionAllDecorator('repository.admin')
321 @HasRepoPermissionLevelDecorator('admin')
367 def edit_permissions_update(self, repo_name):
322 def edit_permissions_update(self, repo_name):
368 form = RepoPermsForm()().to_python(request.POST)
323 form = RepoPermsForm()().to_python(request.POST)
369 RepoModel()._update_permissions(repo_name, form['perms_new'],
324 RepoModel()._update_permissions(repo_name, form['perms_new'],
370 form['perms_updates'])
325 form['perms_updates'])
371 #TODO: implement this
326 # TODO: implement this
372 #action_logger(self.authuser, 'admin_changed_repo_permissions',
327 #action_logger(request.authuser, 'admin_changed_repo_permissions',
373 # repo_name, self.ip_addr, self.sa)
328 # repo_name, request.ip_addr)
374 Session().commit()
329 Session().commit()
375 h.flash(_('Repository permissions updated'), category='success')
330 h.flash(_('Repository permissions updated'), category='success')
376 return redirect(url('edit_repo_perms', repo_name=repo_name))
331 raise HTTPFound(location=url('edit_repo_perms', repo_name=repo_name))
377
332
378 @HasRepoPermissionAllDecorator('repository.admin')
333 @HasRepoPermissionLevelDecorator('admin')
379 def edit_permissions_revoke(self, repo_name):
334 def edit_permissions_revoke(self, repo_name):
380 try:
335 try:
381 obj_type = request.POST.get('obj_type')
336 obj_type = request.POST.get('obj_type')
@@ -384,6 +339,7 b' class ReposController(BaseRepoController'
384 obj_id = safe_int(request.POST.get('user_id'))
339 obj_id = safe_int(request.POST.get('user_id'))
385 elif obj_type == 'user_group':
340 elif obj_type == 'user_group':
386 obj_id = safe_int(request.POST.get('user_group_id'))
341 obj_id = safe_int(request.POST.get('user_group_id'))
342 else: assert False
387
343
388 if obj_type == 'user':
344 if obj_type == 'user':
389 RepoModel().revoke_user_permission(repo=repo_name, user=obj_id)
345 RepoModel().revoke_user_permission(repo=repo_name, user=obj_id)
@@ -391,30 +347,30 b' class ReposController(BaseRepoController'
391 RepoModel().revoke_user_group_permission(
347 RepoModel().revoke_user_group_permission(
392 repo=repo_name, group_name=obj_id
348 repo=repo_name, group_name=obj_id
393 )
349 )
394 #TODO: implement this
350 else: assert False
395 #action_logger(self.authuser, 'admin_revoked_repo_permissions',
351 # TODO: implement this
396 # repo_name, self.ip_addr, self.sa)
352 #action_logger(request.authuser, 'admin_revoked_repo_permissions',
353 # repo_name, request.ip_addr)
397 Session().commit()
354 Session().commit()
398 except Exception:
355 except Exception:
399 log.error(traceback.format_exc())
356 log.error(traceback.format_exc())
400 h.flash(_('An error occurred during revoking of permission'),
357 h.flash(_('An error occurred during revoking of permission'),
401 category='error')
358 category='error')
402 raise HTTPInternalServerError()
359 raise HTTPInternalServerError()
360 return []
403
361
404 @HasRepoPermissionAllDecorator('repository.admin')
362 @HasRepoPermissionLevelDecorator('admin')
405 def edit_fields(self, repo_name):
363 def edit_fields(self, repo_name):
406 """GET /repo_name/settings: Form to edit an existing item"""
364 c.repo_info = self._load_repo()
407 # url('edit_repo', repo_name=ID)
365 c.repo_fields = RepositoryField.query() \
408 c.repo_info = self._load_repo(repo_name)
409 c.repo_fields = RepositoryField.query()\
410 .filter(RepositoryField.repository == c.repo_info).all()
366 .filter(RepositoryField.repository == c.repo_info).all()
411 c.active = 'fields'
367 c.active = 'fields'
412 if request.POST:
368 if request.POST:
413
369
414 return redirect(url('repo_edit_fields'))
370 raise HTTPFound(location=url('repo_edit_fields'))
415 return render('admin/repos/repo_edit.html')
371 return render('admin/repos/repo_edit.html')
416
372
417 @HasRepoPermissionAllDecorator('repository.admin')
373 @HasRepoPermissionLevelDecorator('admin')
418 def create_repo_field(self, repo_name):
374 def create_repo_field(self, repo_name):
419 try:
375 try:
420 form_result = RepoFieldForm()().to_python(dict(request.POST))
376 form_result = RepoFieldForm()().to_python(dict(request.POST))
@@ -427,15 +383,14 b' class ReposController(BaseRepoController'
427 new_field.field_label = form_result['new_field_label']
383 new_field.field_label = form_result['new_field_label']
428 Session().add(new_field)
384 Session().add(new_field)
429 Session().commit()
385 Session().commit()
386 except formencode.Invalid as e:
387 h.flash(_('Field validation error: %s') % e.msg, category='error')
430 except Exception as e:
388 except Exception as e:
431 log.error(traceback.format_exc())
389 log.error(traceback.format_exc())
432 msg = _('An error occurred during creation of field')
390 h.flash(_('An error occurred during creation of field: %r') % e, category='error')
433 if isinstance(e, formencode.Invalid):
391 raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name))
434 msg += ". " + e.msg
435 h.flash(msg, category='error')
436 return redirect(url('edit_repo_fields', repo_name=repo_name))
437
392
438 @HasRepoPermissionAllDecorator('repository.admin')
393 @HasRepoPermissionLevelDecorator('admin')
439 def delete_repo_field(self, repo_name, field_id):
394 def delete_repo_field(self, repo_name, field_id):
440 field = RepositoryField.get_or_404(field_id)
395 field = RepositoryField.get_or_404(field_id)
441 try:
396 try:
@@ -445,39 +400,37 b' class ReposController(BaseRepoController'
445 log.error(traceback.format_exc())
400 log.error(traceback.format_exc())
446 msg = _('An error occurred during removal of field')
401 msg = _('An error occurred during removal of field')
447 h.flash(msg, category='error')
402 h.flash(msg, category='error')
448 return redirect(url('edit_repo_fields', repo_name=repo_name))
403 raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name))
449
404
450 @HasRepoPermissionAllDecorator('repository.admin')
405 @HasRepoPermissionLevelDecorator('admin')
451 def edit_advanced(self, repo_name):
406 def edit_advanced(self, repo_name):
452 """GET /repo_name/settings: Form to edit an existing item"""
407 c.repo_info = self._load_repo()
453 # url('edit_repo', repo_name=ID)
454 c.repo_info = self._load_repo(repo_name)
455 c.default_user_id = User.get_default_user().user_id
408 c.default_user_id = User.get_default_user().user_id
456 c.in_public_journal = UserFollowing.query()\
409 c.in_public_journal = UserFollowing.query() \
457 .filter(UserFollowing.user_id == c.default_user_id)\
410 .filter(UserFollowing.user_id == c.default_user_id) \
458 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
411 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
459
412
460 _repos = Repository.query().order_by(Repository.repo_name).all()
413 _repos = Repository.query(sorted=True).all()
461 read_access_repos = RepoList(_repos)
414 read_access_repos = RepoList(_repos, perm_level='read')
462 c.repos_list = [(None, _('-- Not a fork --'))]
415 c.repos_list = [(None, _('-- Not a fork --'))]
463 c.repos_list += [(x.repo_id, x.repo_name)
416 c.repos_list += [(x.repo_id, x.repo_name)
464 for x in read_access_repos
417 for x in read_access_repos
465 if x.repo_id != c.repo_info.repo_id]
418 if x.repo_id != c.repo_info.repo_id]
466
419
467 defaults = {
420 defaults = {
468 'id_fork_of': c.repo_info.fork.repo_id if c.repo_info.fork else ''
421 'id_fork_of': c.repo_info.fork_id if c.repo_info.fork_id else ''
469 }
422 }
470
423
471 c.active = 'advanced'
424 c.active = 'advanced'
472 if request.POST:
425 if request.POST:
473 return redirect(url('repo_edit_advanced'))
426 raise HTTPFound(location=url('repo_edit_advanced'))
474 return htmlfill.render(
427 return htmlfill.render(
475 render('admin/repos/repo_edit.html'),
428 render('admin/repos/repo_edit.html'),
476 defaults=defaults,
429 defaults=defaults,
477 encoding="UTF-8",
430 encoding="UTF-8",
478 force_defaults=False)
431 force_defaults=False)
479
432
480 @HasRepoPermissionAllDecorator('repository.admin')
433 @HasRepoPermissionLevelDecorator('admin')
481 def edit_advanced_journal(self, repo_name):
434 def edit_advanced_journal(self, repo_name):
482 """
435 """
483 Sets this repository to be visible in public journal,
436 Sets this repository to be visible in public journal,
@@ -497,10 +450,9 b' class ReposController(BaseRepoController'
497 h.flash(_('An error occurred during setting this'
450 h.flash(_('An error occurred during setting this'
498 ' repository in public journal'),
451 ' repository in public journal'),
499 category='error')
452 category='error')
500 return redirect(url('edit_repo_advanced', repo_name=repo_name))
453 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
501
454
502
455 @HasRepoPermissionLevelDecorator('admin')
503 @HasRepoPermissionAllDecorator('repository.admin')
504 def edit_advanced_fork(self, repo_name):
456 def edit_advanced_fork(self, repo_name):
505 """
457 """
506 Mark given repository as a fork of another
458 Mark given repository as a fork of another
@@ -510,7 +462,7 b' class ReposController(BaseRepoController'
510 try:
462 try:
511 fork_id = request.POST.get('id_fork_of')
463 fork_id = request.POST.get('id_fork_of')
512 repo = ScmModel().mark_as_fork(repo_name, fork_id,
464 repo = ScmModel().mark_as_fork(repo_name, fork_id,
513 self.authuser.username)
465 request.authuser.username)
514 fork = repo.fork.repo_name if repo.fork else _('Nothing')
466 fork = repo.fork.repo_name if repo.fork else _('Nothing')
515 Session().commit()
467 Session().commit()
516 h.flash(_('Marked repository %s as fork of %s') % (repo_name, fork),
468 h.flash(_('Marked repository %s as fork of %s') % (repo_name, fork),
@@ -523,9 +475,9 b' class ReposController(BaseRepoController'
523 h.flash(_('An error occurred during this operation'),
475 h.flash(_('An error occurred during this operation'),
524 category='error')
476 category='error')
525
477
526 return redirect(url('edit_repo_advanced', repo_name=repo_name))
478 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
527
479
528 @HasRepoPermissionAllDecorator('repository.admin')
480 @HasRepoPermissionLevelDecorator('admin')
529 def edit_advanced_locking(self, repo_name):
481 def edit_advanced_locking(self, repo_name):
530 """
482 """
531 Unlock repository when it is locked !
483 Unlock repository when it is locked !
@@ -535,7 +487,7 b' class ReposController(BaseRepoController'
535 try:
487 try:
536 repo = Repository.get_by_repo_name(repo_name)
488 repo = Repository.get_by_repo_name(repo_name)
537 if request.POST.get('set_lock'):
489 if request.POST.get('set_lock'):
538 Repository.lock(repo, c.authuser.user_id)
490 Repository.lock(repo, request.authuser.user_id)
539 h.flash(_('Repository has been locked'), category='success')
491 h.flash(_('Repository has been locked'), category='success')
540 elif request.POST.get('set_unlock'):
492 elif request.POST.get('set_unlock'):
541 Repository.unlock(repo)
493 Repository.unlock(repo)
@@ -544,16 +496,10 b' class ReposController(BaseRepoController'
544 log.error(traceback.format_exc())
496 log.error(traceback.format_exc())
545 h.flash(_('An error occurred during unlocking'),
497 h.flash(_('An error occurred during unlocking'),
546 category='error')
498 category='error')
547 return redirect(url('edit_repo_advanced', repo_name=repo_name))
499 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
548
500
549 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
501 @HasRepoPermissionLevelDecorator('write')
550 def toggle_locking(self, repo_name):
502 def toggle_locking(self, repo_name):
551 """
552 Toggle locking of repository by simple GET call to url
553
554 :param repo_name:
555 """
556
557 try:
503 try:
558 repo = Repository.get_by_repo_name(repo_name)
504 repo = Repository.get_by_repo_name(repo_name)
559
505
@@ -562,24 +508,22 b' class ReposController(BaseRepoController'
562 Repository.unlock(repo)
508 Repository.unlock(repo)
563 h.flash(_('Repository has been unlocked'), category='success')
509 h.flash(_('Repository has been unlocked'), category='success')
564 else:
510 else:
565 Repository.lock(repo, c.authuser.user_id)
511 Repository.lock(repo, request.authuser.user_id)
566 h.flash(_('Repository has been locked'), category='success')
512 h.flash(_('Repository has been locked'), category='success')
567
513
568 except Exception as e:
514 except Exception as e:
569 log.error(traceback.format_exc())
515 log.error(traceback.format_exc())
570 h.flash(_('An error occurred during unlocking'),
516 h.flash(_('An error occurred during unlocking'),
571 category='error')
517 category='error')
572 return redirect(url('summary_home', repo_name=repo_name))
518 raise HTTPFound(location=url('summary_home', repo_name=repo_name))
573
519
574 @HasRepoPermissionAllDecorator('repository.admin')
520 @HasRepoPermissionLevelDecorator('admin')
575 def edit_caches(self, repo_name):
521 def edit_caches(self, repo_name):
576 """GET /repo_name/settings: Form to edit an existing item"""
522 c.repo_info = self._load_repo()
577 # url('edit_repo', repo_name=ID)
578 c.repo_info = self._load_repo(repo_name)
579 c.active = 'caches'
523 c.active = 'caches'
580 if request.POST:
524 if request.POST:
581 try:
525 try:
582 ScmModel().mark_for_invalidation(repo_name, delete=True)
526 ScmModel().mark_for_invalidation(repo_name)
583 Session().commit()
527 Session().commit()
584 h.flash(_('Cache invalidation successful'),
528 h.flash(_('Cache invalidation successful'),
585 category='success')
529 category='success')
@@ -588,31 +532,27 b' class ReposController(BaseRepoController'
588 h.flash(_('An error occurred during cache invalidation'),
532 h.flash(_('An error occurred during cache invalidation'),
589 category='error')
533 category='error')
590
534
591 return redirect(url('edit_repo_caches', repo_name=c.repo_name))
535 raise HTTPFound(location=url('edit_repo_caches', repo_name=c.repo_name))
592 return render('admin/repos/repo_edit.html')
536 return render('admin/repos/repo_edit.html')
593
537
594 @HasRepoPermissionAllDecorator('repository.admin')
538 @HasRepoPermissionLevelDecorator('admin')
595 def edit_remote(self, repo_name):
539 def edit_remote(self, repo_name):
596 """GET /repo_name/settings: Form to edit an existing item"""
540 c.repo_info = self._load_repo()
597 # url('edit_repo', repo_name=ID)
598 c.repo_info = self._load_repo(repo_name)
599 c.active = 'remote'
541 c.active = 'remote'
600 if request.POST:
542 if request.POST:
601 try:
543 try:
602 ScmModel().pull_changes(repo_name, self.authuser.username)
544 ScmModel().pull_changes(repo_name, request.authuser.username)
603 h.flash(_('Pulled from remote location'), category='success')
545 h.flash(_('Pulled from remote location'), category='success')
604 except Exception as e:
546 except Exception as e:
605 log.error(traceback.format_exc())
547 log.error(traceback.format_exc())
606 h.flash(_('An error occurred during pull from remote location'),
548 h.flash(_('An error occurred during pull from remote location'),
607 category='error')
549 category='error')
608 return redirect(url('edit_repo_remote', repo_name=c.repo_name))
550 raise HTTPFound(location=url('edit_repo_remote', repo_name=c.repo_name))
609 return render('admin/repos/repo_edit.html')
551 return render('admin/repos/repo_edit.html')
610
552
611 @HasRepoPermissionAllDecorator('repository.admin')
553 @HasRepoPermissionLevelDecorator('admin')
612 def edit_statistics(self, repo_name):
554 def edit_statistics(self, repo_name):
613 """GET /repo_name/settings: Form to edit an existing item"""
555 c.repo_info = self._load_repo()
614 # url('edit_repo', repo_name=ID)
615 c.repo_info = self._load_repo(repo_name)
616 repo = c.repo_info.scm_instance
556 repo = c.repo_info.scm_instance
617
557
618 if c.repo_info.stats:
558 if c.repo_info.stats:
@@ -638,6 +578,6 b' class ReposController(BaseRepoController'
638 log.error(traceback.format_exc())
578 log.error(traceback.format_exc())
639 h.flash(_('An error occurred during deletion of repository stats'),
579 h.flash(_('An error occurred during deletion of repository stats'),
640 category='error')
580 category='error')
641 return redirect(url('edit_repo_statistics', repo_name=c.repo_name))
581 raise HTTPFound(location=url('edit_repo_statistics', repo_name=c.repo_name))
642
582
643 return render('admin/repos/repo_edit.html')
583 return render('admin/repos/repo_edit.html')
@@ -30,16 +30,18 b' import traceback'
30 import formencode
30 import formencode
31
31
32 from formencode import htmlfill
32 from formencode import htmlfill
33 from pylons import request, tmpl_context as c, url, config
33 from tg import request, tmpl_context as c, config
34 from pylons.controllers.util import redirect
34 from tg.i18n import ugettext as _
35 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound
36
36
37 from kallithea.config.routing import url
37 from kallithea.lib import helpers as h
38 from kallithea.lib import helpers as h
38 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
39 from kallithea.lib.auth import LoginRequired, HasPermissionAnyDecorator
39 from kallithea.lib.base import BaseController, render
40 from kallithea.lib.base import BaseController, render
40 from kallithea.lib.celerylib import tasks, run_task
41 from kallithea.lib.celerylib import tasks
41 from kallithea.lib.exceptions import HgsubversionImportError
42 from kallithea.lib.exceptions import HgsubversionImportError
42 from kallithea.lib.utils import repo2db_mapper, set_app_settings
43 from kallithea.lib.utils import repo2db_mapper, set_app_settings
44 from kallithea.lib.vcs import VCSError
43 from kallithea.model.db import Ui, Repository, Setting
45 from kallithea.model.db import Ui, Repository, Setting
44 from kallithea.model.forms import ApplicationSettingsForm, \
46 from kallithea.model.forms import ApplicationSettingsForm, \
45 ApplicationUiSettingsForm, ApplicationVisualisationForm
47 ApplicationUiSettingsForm, ApplicationVisualisationForm
@@ -57,38 +59,30 b' class SettingsController(BaseController)'
57 # map.resource('setting', 'settings', controller='admin/settings',
59 # map.resource('setting', 'settings', controller='admin/settings',
58 # path_prefix='/admin', name_prefix='admin_')
60 # path_prefix='/admin', name_prefix='admin_')
59
61
60 @LoginRequired()
62 @LoginRequired(allow_default_user=True)
61 def __before__(self):
63 def _before(self, *args, **kwargs):
62 super(SettingsController, self).__before__()
64 super(SettingsController, self)._before(*args, **kwargs)
63
65
64 def _get_hg_ui_settings(self):
66 def _get_hg_ui_settings(self):
65 ret = Ui.query().all()
67 ret = Ui.query().all()
66
68
67 if not ret:
68 raise Exception('Could not get application ui settings !')
69 settings = {}
69 settings = {}
70 for each in ret:
70 for each in ret:
71 k = each.ui_key
71 k = each.ui_section + '_' + each.ui_key
72 v = each.ui_value
72 v = each.ui_value
73 if k == '/':
73 if k == 'paths_/':
74 k = 'root_path'
74 k = 'paths_root_path'
75
75
76 if k == 'push_ssl':
76 k = k.replace('.', '_')
77 v = str2bool(v)
78
79 if k.find('.') != -1:
80 k = k.replace('.', '_')
81
77
82 if each.ui_section in ['hooks', 'extensions']:
78 if each.ui_section in ['hooks', 'extensions']:
83 v = each.ui_active
79 v = each.ui_active
84
80
85 settings[each.ui_section + '_' + k] = v
81 settings[k] = v
86 return settings
82 return settings
87
83
88 @HasPermissionAllDecorator('hg.admin')
84 @HasPermissionAnyDecorator('hg.admin')
89 def settings_vcs(self):
85 def settings_vcs(self):
90 """GET /admin/settings: All items in the collection"""
91 # url('admin_settings')
92 c.active = 'vcs'
86 c.active = 'vcs'
93 if request.POST:
87 if request.POST:
94 application_form = ApplicationUiSettingsForm()()
88 application_form = ApplicationUiSettingsForm()()
@@ -104,66 +98,37 b' class SettingsController(BaseController)'
104 force_defaults=False)
98 force_defaults=False)
105
99
106 try:
100 try:
107 sett = Ui.get_by_key('push_ssl')
108 sett.ui_value = form_result['web_push_ssl']
109 Session().add(sett)
110 if c.visual.allow_repo_location_change:
101 if c.visual.allow_repo_location_change:
111 sett = Ui.get_by_key('/')
102 sett = Ui.get_by_key('paths', '/')
112 sett.ui_value = form_result['paths_root_path']
103 sett.ui_value = form_result['paths_root_path']
113 Session().add(sett)
114
104
115 #HOOKS
105 # HOOKS
116 sett = Ui.get_by_key(Ui.HOOK_UPDATE)
106 sett = Ui.get_by_key('hooks', Ui.HOOK_UPDATE)
117 sett.ui_active = form_result['hooks_changegroup_update']
107 sett.ui_active = form_result['hooks_changegroup_update']
118 Session().add(sett)
119
108
120 sett = Ui.get_by_key(Ui.HOOK_REPO_SIZE)
109 sett = Ui.get_by_key('hooks', Ui.HOOK_REPO_SIZE)
121 sett.ui_active = form_result['hooks_changegroup_repo_size']
110 sett.ui_active = form_result['hooks_changegroup_repo_size']
122 Session().add(sett)
123
111
124 sett = Ui.get_by_key(Ui.HOOK_PUSH)
112 sett = Ui.get_by_key('hooks', Ui.HOOK_PUSH_LOG)
125 sett.ui_active = form_result['hooks_changegroup_push_logger']
113 sett.ui_active = form_result['hooks_changegroup_push_logger']
126 Session().add(sett)
127
114
128 sett = Ui.get_by_key(Ui.HOOK_PULL)
115 sett = Ui.get_by_key('hooks', Ui.HOOK_PULL_LOG)
129 sett.ui_active = form_result['hooks_outgoing_pull_logger']
116 sett.ui_active = form_result['hooks_outgoing_pull_logger']
130
117
131 Session().add(sett)
132
133 ## EXTENSIONS
118 ## EXTENSIONS
134 sett = Ui.get_by_key('largefiles')
119 sett = Ui.get_or_create('extensions', 'largefiles')
135 if not sett:
136 #make one if it's not there !
137 sett = Ui()
138 sett.ui_key = 'largefiles'
139 sett.ui_section = 'extensions'
140 sett.ui_active = form_result['extensions_largefiles']
120 sett.ui_active = form_result['extensions_largefiles']
141 Session().add(sett)
142
121
143 sett = Ui.get_by_key('hgsubversion')
122 sett = Ui.get_or_create('extensions', 'hgsubversion')
144 if not sett:
145 #make one if it's not there !
146 sett = Ui()
147 sett.ui_key = 'hgsubversion'
148 sett.ui_section = 'extensions'
149
150 sett.ui_active = form_result['extensions_hgsubversion']
123 sett.ui_active = form_result['extensions_hgsubversion']
151 if sett.ui_active:
124 if sett.ui_active:
152 try:
125 try:
153 import hgsubversion # pragma: no cover
126 import hgsubversion # pragma: no cover
154 except ImportError:
127 except ImportError:
155 raise HgsubversionImportError
128 raise HgsubversionImportError
156 Session().add(sett)
157
129
158 # sett = Ui.get_by_key('hggit')
130 # sett = Ui.get_or_create('extensions', 'hggit')
159 # if not sett:
160 # #make one if it's not there !
161 # sett = Ui()
162 # sett.ui_key = 'hggit'
163 # sett.ui_section = 'extensions'
164 #
165 # sett.ui_active = form_result['extensions_hggit']
131 # sett.ui_active = form_result['extensions_hggit']
166 # Session().add(sett)
167
132
168 Session().commit()
133 Session().commit()
169
134
@@ -189,15 +154,13 b' class SettingsController(BaseController)'
189 encoding="UTF-8",
154 encoding="UTF-8",
190 force_defaults=False)
155 force_defaults=False)
191
156
192 @HasPermissionAllDecorator('hg.admin')
157 @HasPermissionAnyDecorator('hg.admin')
193 def settings_mapping(self):
158 def settings_mapping(self):
194 """GET /admin/settings/mapping: All items in the collection"""
195 # url('admin_settings_mapping')
196 c.active = 'mapping'
159 c.active = 'mapping'
197 if request.POST:
160 if request.POST:
198 rm_obsolete = request.POST.get('destroy', False)
161 rm_obsolete = request.POST.get('destroy', False)
199 install_git_hooks = request.POST.get('hooks', False)
162 install_git_hooks = request.POST.get('hooks', False)
200 overwrite_git_hooks = request.POST.get('hooks_overwrite', False);
163 overwrite_git_hooks = request.POST.get('hooks_overwrite', False)
201 invalidate_cache = request.POST.get('invalidate', False)
164 invalidate_cache = request.POST.get('invalidate', False)
202 log.debug('rescanning repo location with destroy obsolete=%s, '
165 log.debug('rescanning repo location with destroy obsolete=%s, '
203 'install git hooks=%s and '
166 'install git hooks=%s and '
@@ -206,7 +169,7 b' class SettingsController(BaseController)'
206 filesystem_repos = ScmModel().repo_scan()
169 filesystem_repos = ScmModel().repo_scan()
207 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete,
170 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete,
208 install_git_hooks=install_git_hooks,
171 install_git_hooks=install_git_hooks,
209 user=c.authuser.username,
172 user=request.authuser.username,
210 overwrite_git_hooks=overwrite_git_hooks)
173 overwrite_git_hooks=overwrite_git_hooks)
211 h.flash(h.literal(_('Repositories successfully rescanned. Added: %s. Removed: %s.') %
174 h.flash(h.literal(_('Repositories successfully rescanned. Added: %s. Removed: %s.') %
212 (', '.join(h.link_to(safe_unicode(repo_name), h.url('summary_home', repo_name=repo_name))
175 (', '.join(h.link_to(safe_unicode(repo_name), h.url('summary_home', repo_name=repo_name))
@@ -217,15 +180,15 b' class SettingsController(BaseController)'
217 if invalidate_cache:
180 if invalidate_cache:
218 log.debug('invalidating all repositories cache')
181 log.debug('invalidating all repositories cache')
219 i = 0
182 i = 0
220 for repo in Repository.get_all():
183 for repo in Repository.query():
221 try:
184 try:
222 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
185 ScmModel().mark_for_invalidation(repo.repo_name)
223 i += 1
186 i += 1
224 except VCSError as e:
187 except VCSError as e:
225 log.warning('VCS error invalidating %s: %s', repo.repo_name, e)
188 log.warning('VCS error invalidating %s: %s', repo.repo_name, e)
226 h.flash(_('Invalidated %s repositories') % i, category='success')
189 h.flash(_('Invalidated %s repositories') % i, category='success')
227
190
228 return redirect(url('admin_settings_mapping'))
191 raise HTTPFound(location=url('admin_settings_mapping'))
229
192
230 defaults = Setting.get_app_settings()
193 defaults = Setting.get_app_settings()
231 defaults.update(self._get_hg_ui_settings())
194 defaults.update(self._get_hg_ui_settings())
@@ -236,10 +199,8 b' class SettingsController(BaseController)'
236 encoding="UTF-8",
199 encoding="UTF-8",
237 force_defaults=False)
200 force_defaults=False)
238
201
239 @HasPermissionAllDecorator('hg.admin')
202 @HasPermissionAnyDecorator('hg.admin')
240 def settings_global(self):
203 def settings_global(self):
241 """GET /admin/settings/global: All items in the collection"""
242 # url('admin_settings_global')
243 c.active = 'global'
204 c.active = 'global'
244 if request.POST:
205 if request.POST:
245 application_form = ApplicationSettingsForm()()
206 application_form = ApplicationSettingsForm()()
@@ -255,25 +216,14 b' class SettingsController(BaseController)'
255 force_defaults=False)
216 force_defaults=False)
256
217
257 try:
218 try:
258 sett1 = Setting.create_or_update('title',
219 for setting in (
259 form_result['title'])
220 'title',
260 Session().add(sett1)
221 'realm',
261
222 'ga_code',
262 sett2 = Setting.create_or_update('realm',
223 'captcha_public_key',
263 form_result['realm'])
224 'captcha_private_key',
264 Session().add(sett2)
225 ):
265
226 Setting.create_or_update(setting, form_result[setting])
266 sett3 = Setting.create_or_update('ga_code',
267 form_result['ga_code'])
268 Session().add(sett3)
269
270 sett4 = Setting.create_or_update('captcha_public_key',
271 form_result['captcha_public_key'])
272 Session().add(sett4)
273
274 sett5 = Setting.create_or_update('captcha_private_key',
275 form_result['captcha_private_key'])
276 Session().add(sett5)
277
227
278 Session().commit()
228 Session().commit()
279 set_app_settings(config)
229 set_app_settings(config)
@@ -285,7 +235,7 b' class SettingsController(BaseController)'
285 'application settings'),
235 'application settings'),
286 category='error')
236 category='error')
287
237
288 return redirect(url('admin_settings_global'))
238 raise HTTPFound(location=url('admin_settings_global'))
289
239
290 defaults = Setting.get_app_settings()
240 defaults = Setting.get_app_settings()
291 defaults.update(self._get_hg_ui_settings())
241 defaults.update(self._get_hg_ui_settings())
@@ -296,10 +246,8 b' class SettingsController(BaseController)'
296 encoding="UTF-8",
246 encoding="UTF-8",
297 force_defaults=False)
247 force_defaults=False)
298
248
299 @HasPermissionAllDecorator('hg.admin')
249 @HasPermissionAnyDecorator('hg.admin')
300 def settings_visual(self):
250 def settings_visual(self):
301 """GET /admin/settings/visual: All items in the collection"""
302 # url('admin_settings_visual')
303 c.active = 'visual'
251 c.active = 'visual'
304 if request.POST:
252 if request.POST:
305 application_form = ApplicationVisualisationForm()()
253 application_form = ApplicationVisualisationForm()()
@@ -318,7 +266,7 b' class SettingsController(BaseController)'
318 settings = [
266 settings = [
319 ('show_public_icon', 'show_public_icon', 'bool'),
267 ('show_public_icon', 'show_public_icon', 'bool'),
320 ('show_private_icon', 'show_private_icon', 'bool'),
268 ('show_private_icon', 'show_private_icon', 'bool'),
321 ('stylify_metatags', 'stylify_metatags', 'bool'),
269 ('stylify_metalabels', 'stylify_metalabels', 'bool'),
322 ('repository_fields', 'repository_fields', 'bool'),
270 ('repository_fields', 'repository_fields', 'bool'),
323 ('dashboard_items', 'dashboard_items', 'int'),
271 ('dashboard_items', 'dashboard_items', 'int'),
324 ('admin_grid_items', 'admin_grid_items', 'int'),
272 ('admin_grid_items', 'admin_grid_items', 'int'),
@@ -328,9 +276,7 b' class SettingsController(BaseController)'
328 ('clone_uri_tmpl', 'clone_uri_tmpl', 'unicode'),
276 ('clone_uri_tmpl', 'clone_uri_tmpl', 'unicode'),
329 ]
277 ]
330 for setting, form_key, type_ in settings:
278 for setting, form_key, type_ in settings:
331 sett = Setting.create_or_update(setting,
279 Setting.create_or_update(setting, form_result[form_key], type_)
332 form_result[form_key], type_)
333 Session().add(sett)
334
280
335 Session().commit()
281 Session().commit()
336 set_app_settings(config)
282 set_app_settings(config)
@@ -343,7 +289,7 b' class SettingsController(BaseController)'
343 'visualisation settings'),
289 'visualisation settings'),
344 category='error')
290 category='error')
345
291
346 return redirect(url('admin_settings_visual'))
292 raise HTTPFound(location=url('admin_settings_visual'))
347
293
348 defaults = Setting.get_app_settings()
294 defaults = Setting.get_app_settings()
349 defaults.update(self._get_hg_ui_settings())
295 defaults.update(self._get_hg_ui_settings())
@@ -354,10 +300,8 b' class SettingsController(BaseController)'
354 encoding="UTF-8",
300 encoding="UTF-8",
355 force_defaults=False)
301 force_defaults=False)
356
302
357 @HasPermissionAllDecorator('hg.admin')
303 @HasPermissionAnyDecorator('hg.admin')
358 def settings_email(self):
304 def settings_email(self):
359 """GET /admin/settings/email: All items in the collection"""
360 # url('admin_settings_email')
361 c.active = 'email'
305 c.active = 'email'
362 if request.POST:
306 if request.POST:
363 test_email = request.POST.get('test_email')
307 test_email = request.POST.get('test_email')
@@ -366,22 +310,22 b' class SettingsController(BaseController)'
366 'Kallithea version: %s' % c.kallithea_version)
310 'Kallithea version: %s' % c.kallithea_version)
367 if not test_email:
311 if not test_email:
368 h.flash(_('Please enter email address'), category='error')
312 h.flash(_('Please enter email address'), category='error')
369 return redirect(url('admin_settings_email'))
313 raise HTTPFound(location=url('admin_settings_email'))
370
314
371 test_email_txt_body = EmailNotificationModel()\
315 test_email_txt_body = EmailNotificationModel() \
372 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
316 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
373 'txt', body=test_body)
317 'txt', body=test_body)
374 test_email_html_body = EmailNotificationModel()\
318 test_email_html_body = EmailNotificationModel() \
375 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
319 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
376 'html', body=test_body)
320 'html', body=test_body)
377
321
378 recipients = [test_email] if test_email else None
322 recipients = [test_email] if test_email else None
379
323
380 run_task(tasks.send_email, recipients, test_email_subj,
324 tasks.send_email(recipients, test_email_subj,
381 test_email_txt_body, test_email_html_body)
325 test_email_txt_body, test_email_html_body)
382
326
383 h.flash(_('Send email task created'), category='success')
327 h.flash(_('Send email task created'), category='success')
384 return redirect(url('admin_settings_email'))
328 raise HTTPFound(location=url('admin_settings_email'))
385
329
386 defaults = Setting.get_app_settings()
330 defaults = Setting.get_app_settings()
387 defaults.update(self._get_hg_ui_settings())
331 defaults.update(self._get_hg_ui_settings())
@@ -395,10 +339,8 b' class SettingsController(BaseController)'
395 encoding="UTF-8",
339 encoding="UTF-8",
396 force_defaults=False)
340 force_defaults=False)
397
341
398 @HasPermissionAllDecorator('hg.admin')
342 @HasPermissionAnyDecorator('hg.admin')
399 def settings_hooks(self):
343 def settings_hooks(self):
400 """GET /admin/settings/hooks: All items in the collection"""
401 # url('admin_settings_hooks')
402 c.active = 'hooks'
344 c.active = 'hooks'
403 if request.POST:
345 if request.POST:
404 if c.visual.allow_custom_hooks_settings:
346 if c.visual.allow_custom_hooks_settings:
@@ -409,7 +351,11 b' class SettingsController(BaseController)'
409
351
410 try:
352 try:
411 ui_key = ui_key and ui_key.strip()
353 ui_key = ui_key and ui_key.strip()
412 if ui_value and ui_key:
354 if ui_key in (x.ui_key for x in Ui.get_custom_hooks()):
355 h.flash(_('Hook already exists'), category='error')
356 elif ui_key in (x.ui_key for x in Ui.get_builtin_hooks()):
357 h.flash(_('Builtin hooks are read-only. Please use another hook name.'), category='error')
358 elif ui_value and ui_key:
413 Ui.create_or_update_hook(ui_key, ui_value)
359 Ui.create_or_update_hook(ui_key, ui_value)
414 h.flash(_('Added new hook'), category='success')
360 h.flash(_('Added new hook'), category='success')
415 elif hook_id:
361 elif hook_id:
@@ -419,10 +365,12 b' class SettingsController(BaseController)'
419 # check for edits
365 # check for edits
420 update = False
366 update = False
421 _d = request.POST.dict_of_lists()
367 _d = request.POST.dict_of_lists()
422 for k, v in zip(_d.get('hook_ui_key', []),
368 for k, v, ov in zip(_d.get('hook_ui_key', []),
423 _d.get('hook_ui_value_new', [])):
369 _d.get('hook_ui_value_new', []),
424 Ui.create_or_update_hook(k, v)
370 _d.get('hook_ui_value', [])):
425 update = True
371 if v != ov:
372 Ui.create_or_update_hook(k, v)
373 update = True
426
374
427 if update:
375 if update:
428 h.flash(_('Updated hooks'), category='success')
376 h.flash(_('Updated hooks'), category='success')
@@ -432,7 +380,7 b' class SettingsController(BaseController)'
432 h.flash(_('Error occurred during hook creation'),
380 h.flash(_('Error occurred during hook creation'),
433 category='error')
381 category='error')
434
382
435 return redirect(url('admin_settings_hooks'))
383 raise HTTPFound(location=url('admin_settings_hooks'))
436
384
437 defaults = Setting.get_app_settings()
385 defaults = Setting.get_app_settings()
438 defaults.update(self._get_hg_ui_settings())
386 defaults.update(self._get_hg_ui_settings())
@@ -446,17 +394,15 b' class SettingsController(BaseController)'
446 encoding="UTF-8",
394 encoding="UTF-8",
447 force_defaults=False)
395 force_defaults=False)
448
396
449 @HasPermissionAllDecorator('hg.admin')
397 @HasPermissionAnyDecorator('hg.admin')
450 def settings_search(self):
398 def settings_search(self):
451 """GET /admin/settings/search: All items in the collection"""
452 # url('admin_settings_search')
453 c.active = 'search'
399 c.active = 'search'
454 if request.POST:
400 if request.POST:
455 repo_location = self._get_hg_ui_settings()['paths_root_path']
401 repo_location = self._get_hg_ui_settings()['paths_root_path']
456 full_index = request.POST.get('full_index', False)
402 full_index = request.POST.get('full_index', False)
457 run_task(tasks.whoosh_index, repo_location, full_index)
403 tasks.whoosh_index(repo_location, full_index)
458 h.flash(_('Whoosh reindex task scheduled'), category='success')
404 h.flash(_('Whoosh reindex task scheduled'), category='success')
459 return redirect(url('admin_settings_search'))
405 raise HTTPFound(location=url('admin_settings_search'))
460
406
461 defaults = Setting.get_app_settings()
407 defaults = Setting.get_app_settings()
462 defaults.update(self._get_hg_ui_settings())
408 defaults.update(self._get_hg_ui_settings())
@@ -467,10 +413,8 b' class SettingsController(BaseController)'
467 encoding="UTF-8",
413 encoding="UTF-8",
468 force_defaults=False)
414 force_defaults=False)
469
415
470 @HasPermissionAllDecorator('hg.admin')
416 @HasPermissionAnyDecorator('hg.admin')
471 def settings_system(self):
417 def settings_system(self):
472 """GET /admin/settings/system: All items in the collection"""
473 # url('admin_settings_system')
474 c.active = 'system'
418 c.active = 'system'
475
419
476 defaults = Setting.get_app_settings()
420 defaults = Setting.get_app_settings()
@@ -489,10 +433,8 b' class SettingsController(BaseController)'
489 encoding="UTF-8",
433 encoding="UTF-8",
490 force_defaults=False)
434 force_defaults=False)
491
435
492 @HasPermissionAllDecorator('hg.admin')
436 @HasPermissionAnyDecorator('hg.admin')
493 def settings_system_update(self):
437 def settings_system_update(self):
494 """GET /admin/settings/system/updates: All items in the collection"""
495 # url('admin_settings_system_update')
496 import json
438 import json
497 import urllib2
439 import urllib2
498 from kallithea.lib.verlib import NormalizedVersion
440 from kallithea.lib.verlib import NormalizedVersion
@@ -503,7 +445,7 b' class SettingsController(BaseController)'
503 _update_url = defaults.get('update_url', '')
445 _update_url = defaults.get('update_url', '')
504 _update_url = "" # FIXME: disabled
446 _update_url = "" # FIXME: disabled
505
447
506 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">%s</div>' % (s)
448 _err = lambda s: '<div class="alert alert-danger">%s</div>' % (s)
507 try:
449 try:
508 import kallithea
450 import kallithea
509 ver = kallithea.__version__
451 ver = kallithea.__version__
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from kallithea/public/css/style.css to kallithea/front-end/style.less
NO CONTENT: file renamed from kallithea/public/css/style.css to kallithea/front-end/style.less
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file copied from kallithea/lib/utils2.py to kallithea/lib/pygmentsutils.py
NO CONTENT: file copied from kallithea/lib/utils2.py to kallithea/lib/pygmentsutils.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file copied from kallithea/model/__init__.py to kallithea/model/base.py
NO CONTENT: file copied from kallithea/model/__init__.py to kallithea/model/base.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file copied from kallithea/tests/__init__.py to kallithea/tests/base.py
NO CONTENT: file copied from kallithea/tests/__init__.py to kallithea/tests/base.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from kallithea/tests/functional/test_changeset_comments.py to kallithea/tests/functional/test_changeset_pullrequests_comments.py
NO CONTENT: file renamed from kallithea/tests/functional/test_changeset_comments.py to kallithea/tests/functional/test_changeset_pullrequests_comments.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from kallithea/tests/other/manual_test_vcs_operations.py to kallithea/tests/other/test_vcs_operations.py
NO CONTENT: file renamed from kallithea/tests/other/manual_test_vcs_operations.py to kallithea/tests/other/test_vcs_operations.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
This diff has been collapsed as it changes many lines, (591 lines changed) Show them Hide them
1 NO CONTENT: file was removed
NO CONTENT: file was removed
This diff has been collapsed as it changes many lines, (580 lines changed) Show them Hide them
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed, binary diff hidden
NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed, binary diff hidden
NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed, binary diff hidden
NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed, binary diff hidden
NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed, binary diff hidden
NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now