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,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 é (é) | |||
|
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 © 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 © 2012 Twitter, Inc. |
|
44 | Copyright © 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 |
|
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 © 2013-2014 by Marijn Haverbeke <marijnh@gmail.com> |
|
60 | Copyright © 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. |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 © 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 © 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 |
|
192 | Kallithea uses some parts of a Javascript system called | |
232 |
[Flot](http:// |
|
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) 201 |
|
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/ |
|
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/ |
|
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 |
|
|
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, |
|
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 |
|
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 = |
|
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 |
|
|
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 sep |
|
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 commit |
|
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 = |
|
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 = |
|
204 | #issue_pat_wiki = wiki-(\S+) | |
280 |
#issue_server_link_wiki = https://wiki.example.com/ |
|
205 | #issue_server_link_wiki = https://wiki.example.com/\1 | |
281 |
#issue_ |
|
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: * |
|
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 |
# |
|
318 | #debug = false | |
468 |
|
|
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. |
|
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 = |
|
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 |
|
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 |
|
94 | Plain unencrypted LDAP connection. | |
|
95 | This will by default use `Port`_ 389. | |||
241 |
|
96 | |||
242 |
LDAPS |
|
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 |
|
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 |
|
268 | After setting up your proxy solution (see :ref:`apache_virtual_host_reverse_proxy`, | |
404 |
|
|
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_sup |
|
1 | .. _vcs_setup: | |
2 |
|
2 | |||
3 |
============================= |
|
3 | ============================= | |
4 |
Version control systems sup |
|
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 ` |
|
32 | waitress_ for `gearbox serve` instead of the built-in `paste` WSGI | |
32 | server. |
|
33 | server. | |
33 |
|
34 | |||
34 |
The |
|
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 spe |
|
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 |
|
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. |
|
86 | After finishing your changes make sure all tests pass cleanly. Run the testsuite | |
52 |
|
|
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 |
|
|
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 |
|
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 |
Co |
|
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 |
|
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_ |
|
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 |
|
|
57 | . ../kallithea-venv/bin/activate | |
42 |
pip install --upgrade pip |
|
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 |
|
|
84 | . /srv/kallithea/venv/bin/activate | |
72 |
pip install --upgrade pip |
|
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 |
|
114 | - This will install Kallithea together with all other required | |
102 |
|
|
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 |
|
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
17 | :help | |
18 |
|
|
18 | echo.Please use `make ^<target^>` where ^<target^> is one of | |
19 |
|
|
19 | echo. html to make standalone HTML files | |
20 |
|
|
20 | echo. dirhtml to make HTML files named index.html in directories | |
21 |
|
|
21 | echo. singlehtml to make a single large HTML file | |
22 |
|
|
22 | echo. pickle to make pickle files | |
23 |
|
|
23 | echo. json to make JSON files | |
24 |
|
|
24 | echo. htmlhelp to make HTML files and a HTML help project | |
25 |
|
|
25 | echo. qthelp to make HTML files and a qthelp project | |
26 |
|
|
26 | echo. devhelp to make HTML files and a Devhelp project | |
27 |
|
|
27 | echo. epub to make an epub | |
28 |
|
|
28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter | |
29 |
|
|
29 | echo. text to make text files | |
30 |
|
|
30 | echo. man to make manual pages | |
31 |
|
|
31 | echo. changes to make an overview over all changed/added/deprecated items | |
32 |
|
|
32 | echo. linkcheck to check all external links for integrity | |
33 |
|
|
33 | echo. doctest to run all doctests embedded in the documentation if enabled | |
34 |
|
|
34 | goto end | |
35 | ) |
|
35 | ) | |
36 |
|
36 | |||
37 | if "%1" == "clean" ( |
|
37 | if "%1" == "clean" ( | |
38 |
|
|
38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i | |
39 |
|
|
39 | del /q /s %BUILDDIR%\* | |
40 |
|
|
40 | goto end | |
41 | ) |
|
41 | ) | |
42 |
|
42 | |||
43 | if "%1" == "html" ( |
|
43 | if "%1" == "html" ( | |
44 |
|
|
44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html | |
45 |
|
|
45 | echo. | |
46 |
|
|
46 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. | |
47 |
|
|
47 | goto end | |
48 | ) |
|
48 | ) | |
49 |
|
49 | |||
50 | if "%1" == "dirhtml" ( |
|
50 | if "%1" == "dirhtml" ( | |
51 |
|
|
51 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml | |
52 |
|
|
52 | echo. | |
53 |
|
|
53 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. | |
54 |
|
|
54 | goto end | |
55 | ) |
|
55 | ) | |
56 |
|
56 | |||
57 | if "%1" == "singlehtml" ( |
|
57 | if "%1" == "singlehtml" ( | |
58 |
|
|
58 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml | |
59 |
|
|
59 | echo. | |
60 |
|
|
60 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. | |
61 |
|
|
61 | goto end | |
62 | ) |
|
62 | ) | |
63 |
|
63 | |||
64 | if "%1" == "pickle" ( |
|
64 | if "%1" == "pickle" ( | |
65 |
|
|
65 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle | |
66 |
|
|
66 | echo. | |
67 |
|
|
67 | echo.Build finished; now you can process the pickle files. | |
68 |
|
|
68 | goto end | |
69 | ) |
|
69 | ) | |
70 |
|
70 | |||
71 | if "%1" == "json" ( |
|
71 | if "%1" == "json" ( | |
72 |
|
|
72 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json | |
73 |
|
|
73 | echo. | |
74 |
|
|
74 | echo.Build finished; now you can process the JSON files. | |
75 |
|
|
75 | goto end | |
76 | ) |
|
76 | ) | |
77 |
|
77 | |||
78 | if "%1" == "htmlhelp" ( |
|
78 | if "%1" == "htmlhelp" ( | |
79 |
|
|
79 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp | |
80 |
|
|
80 | echo. | |
81 |
|
|
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 |
|
|
83 | goto end | |
84 | ) |
|
84 | ) | |
85 |
|
85 | |||
86 | if "%1" == "qthelp" ( |
|
86 | if "%1" == "qthelp" ( | |
87 |
|
|
87 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp | |
88 |
|
|
88 | echo. | |
89 |
|
|
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 |
|
|
91 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Kallithea.qhcp | |
92 |
|
|
92 | echo.To view the help file: | |
93 |
|
|
93 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Kallithea.ghc | |
94 |
|
|
94 | goto end | |
95 | ) |
|
95 | ) | |
96 |
|
96 | |||
97 | if "%1" == "devhelp" ( |
|
97 | if "%1" == "devhelp" ( | |
98 |
|
|
98 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp | |
99 |
|
|
99 | echo. | |
100 |
|
|
100 | echo.Build finished. | |
101 |
|
|
101 | goto end | |
102 | ) |
|
102 | ) | |
103 |
|
103 | |||
104 | if "%1" == "epub" ( |
|
104 | if "%1" == "epub" ( | |
105 |
|
|
105 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub | |
106 |
|
|
106 | echo. | |
107 |
|
|
107 | echo.Build finished. The epub file is in %BUILDDIR%/epub. | |
108 |
|
|
108 | goto end | |
109 | ) |
|
109 | ) | |
110 |
|
110 | |||
111 | if "%1" == "latex" ( |
|
111 | if "%1" == "latex" ( | |
112 |
|
|
112 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex | |
113 |
|
|
113 | echo. | |
114 |
|
|
114 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. | |
115 |
|
|
115 | goto end | |
116 | ) |
|
116 | ) | |
117 |
|
117 | |||
118 | if "%1" == "text" ( |
|
118 | if "%1" == "text" ( | |
119 |
|
|
119 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text | |
120 |
|
|
120 | echo. | |
121 |
|
|
121 | echo.Build finished. The text files are in %BUILDDIR%/text. | |
122 |
|
|
122 | goto end | |
123 | ) |
|
123 | ) | |
124 |
|
124 | |||
125 | if "%1" == "man" ( |
|
125 | if "%1" == "man" ( | |
126 |
|
|
126 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man | |
127 |
|
|
127 | echo. | |
128 |
|
|
128 | echo.Build finished. The manual pages are in %BUILDDIR%/man. | |
129 |
|
|
129 | goto end | |
130 | ) |
|
130 | ) | |
131 |
|
131 | |||
132 | if "%1" == "changes" ( |
|
132 | if "%1" == "changes" ( | |
133 |
|
|
133 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes | |
134 |
|
|
134 | echo. | |
135 |
|
|
135 | echo.The overview file is in %BUILDDIR%/changes. | |
136 |
|
|
136 | goto end | |
137 | ) |
|
137 | ) | |
138 |
|
138 | |||
139 | if "%1" == "linkcheck" ( |
|
139 | if "%1" == "linkcheck" ( | |
140 |
|
|
140 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck | |
141 |
|
|
141 | echo. | |
142 |
|
|
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 |
|
|
144 | goto end | |
145 | ) |
|
145 | ) | |
146 |
|
146 | |||
147 | if "%1" == "doctest" ( |
|
147 | if "%1" == "doctest" ( | |
148 |
|
|
148 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest | |
149 |
|
|
149 | echo. | |
150 |
|
|
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 |
|
|
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 |
|
92 | - Kallithea uses the Gearbox_ tool as command line interface. Gearbox provides | |
88 |
`` |
|
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 |
|
|
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 `` |
|
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 `` |
|
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 `` |
|
34 | entering this "root" path ``db-create`` will also prompt you for a username | |
33 |
and password for the initial admin account which `` |
|
35 | and password for the initial admin account which ``db-create`` sets | |
34 | up for you. |
|
36 | up for you. | |
35 |
|
37 | |||
36 |
The `` |
|
38 | The ``db-create`` values can also be given on the command line. | |
37 | Example:: |
|
39 | Example:: | |
38 |
|
40 | |||
39 |
|
|
41 | kallithea-cli db-create -c my.ini --user=nn --password=secret --email=nn@example.com --repos=/srv/repos | |
40 |
|
42 | |||
41 |
The `` |
|
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 |
|
|
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 `` |
|
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 |
|
|
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/ |
|
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. |
|
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 = |
|
172 | issue_pat = #(\d+) | |
447 |
issue_server_link = https://issues.example.com/{repo}/issue/ |
|
173 | issue_server_link = https://issues.example.com/{repo}/issue/\1 | |
448 |
issue_ |
|
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. |
|
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"> |
|
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 = |
|
228 | issue_pat_wiki = wiki-(?P<pagename>\S+) | |
471 |
issue_server_link_wiki = https://wiki.example.com/ |
|
229 | issue_server_link_wiki = https://wiki.example.com/\g<pagename> | |
472 |
issue_ |
|
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 / |
|
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 |
|
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 / |
|
459 | <Location /PREFIX > | |
687 |
ProxyPass http://127.0.0.1:5000/ |
|
460 | ProxyPass http://127.0.0.1:5000/PREFIX | |
688 |
ProxyPassReverse http://127.0.0.1:5000/ |
|
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 = / |
|
474 | prefix = /PREFIX | |
702 |
|
475 | |||
703 |
then change `` |
|
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 |
|
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 |
|
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``), |
|
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 |
|
79 | configuration file -- an email with exception details is sent by backlash_ | |
71 |
|
|
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 |
|
|
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 ``-- |
|
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 |
|
|
8 | slower than expected. Because of the demanding nature of handling large amounts | |
9 |
|
|
9 | of data from version control systems, here are some tips on how to get the best | |
10 |
|
|
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 |
|
|
30 | SQLite is a good option when having a small load on the system. But due to | |
29 |
|
|
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 |
|
|
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 An |
|
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_ |
|
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 |
|
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 |
|
5 | description "Celery for Kallithea Mercurial Server" | |
6 |
author |
|
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/ |
|
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/ |
|
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/ |
|
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/ |
|
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 |
|
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/ |
|
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 |
|
5 | description "Kallithea Mercurial Server" | |
6 |
author |
|
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/ |
|
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 |
|
|
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/ |
|
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 |
|
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, |
|
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 |
|
|
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__ = |
|
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 |
|
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 = |
|
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 |
|
|
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 |
|
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 |
|
|
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: |
|
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_ |
|
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 |
|
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 = |
|
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, |
|
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 |
|
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 |
|
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[' |
|
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(" |
|
121 | m.connect("update_repo", "/repos/{repo_name:.*?}", | |
123 |
action="update", conditions=dict(method=["P |
|
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=[" |
|
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=["P |
|
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=["P |
|
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=[" |
|
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=[" |
|
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("user |
|
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=["P |
|
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=[" |
|
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=[" |
|
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=["P |
|
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=["P |
|
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=[" |
|
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=["P |
|
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=[" |
|
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=["P |
|
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=[" |
|
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=["P |
|
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=["P |
|
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=[" |
|
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 |
|
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=[" |
|
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=[" |
|
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=[" |
|
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 |
|
|
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=["P |
|
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=[" |
|
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=["P |
|
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=[" |
|
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=["P |
|
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=["P |
|
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=["P |
|
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(" |
|
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=["P |
|
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=["P |
|
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=["P |
|
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_ |
|
599 | rmap.connect('changeset_comment_delete', | |
657 |
'/{repo_name:.*?}/changeset-comment |
|
600 | '/{repo_name:.*?}/changeset-comment/{comment_id}/delete', | |
658 |
controller='changeset', action=' |
|
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=[" |
|
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=[" |
|
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 |
|
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, HasPermissionA |
|
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. |
|
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 |
|
124 | def _before(self, *args, **kwargs): | |
124 |
super(AdminController, self). |
|
125 | super(AdminController, self)._before(*args, **kwargs) | |
125 |
|
126 | |||
126 |
@HasPermissionA |
|
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' |
|
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 |
|
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, HasPermissionA |
|
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 |
@HasPermissionA |
|
50 | @HasPermissionAnyDecorator('hg.admin') | |
50 |
def |
|
51 | def _before(self, *args, **kwargs): | |
51 |
super(AuthSettingsController, self). |
|
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 |
|
|
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 |
|
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 = |
|
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 = [ |
|
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 |
re |
|
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 |
|
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, HasPermissionA |
|
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 |
@HasPermissionA |
|
52 | @HasPermissionAnyDecorator('hg.admin') | |
56 |
def |
|
53 | def _before(self, *args, **kwargs): | |
57 |
super(DefaultsController, self). |
|
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 |
re |
|
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 |
|
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 |
|
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. |
|
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. |
|
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. |
|
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. |
|
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' |
|
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= |
|
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 |
re |
|
141 | raise HTTPFound(location=url('new_gist')) | |
148 |
re |
|
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. |
|
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 |
re |
|
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 |
|
|
166 | if c.gist.is_expired: | |
197 | if c.gist.gist_expires != -1: |
|
167 | log.error('Gist expired at %s', | |
198 |
|
|
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 |
|
|
186 | if c.gist.is_expired: | |
222 | if c.gist.gist_expires != -1: |
|
187 | log.error('Gist expired at %s', | |
223 |
|
|
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 |
re |
|
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 |
# |
|
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 |
|
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, |
|
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( |
|
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 |
re |
|
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 = |
|
75 | repos_list = Session().query(Repository) \ | |
79 |
|
|
76 | .join(UserFollowing) \ | |
80 |
|
|
77 | .filter(UserFollowing.user_id == | |
81 |
|
|
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. |
|
82 | .filter(Repository.owner_id == | |
86 |
|
|
83 | request.authuser.user_id).all() | |
87 | .order_by(func.lower(Repository.repo_name)).all() |
|
|||
88 |
|
84 | |||
89 |
re |
|
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= |
|
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() |
|
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': |
|
102 | old_data={'user_id': request.authuser.user_id, | |
115 |
'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( |
|
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 |
re |
|
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( |
|
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( |
|
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 |
# |
|
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 |
# |
|
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= |
|
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( |
|
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 |
re |
|
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( |
|
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 |
re |
|
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( |
|
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( |
|
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 |
re |
|
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, |
|
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 |
re |
|
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 |
|
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, HasPermissionA |
|
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 |
@HasPermissionA |
|
57 | @HasPermissionAnyDecorator('hg.admin') | |
57 |
def |
|
58 | def _before(self, *args, **kwargs): | |
58 |
super(PermissionsController, self). |
|
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 |
re |
|
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 |
|
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 |
|
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 |
HasRepoGroupPermission |
|
43 | HasRepoGroupPermissionLevelDecorator, HasRepoGroupPermissionLevel, \ | |
44 |
HasPermissionA |
|
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 |
|
62 | def _before(self, *args, **kwargs): | |
64 |
super(RepoGroupsController, self). |
|
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([], |
|
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: |
|
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: |
|
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 = |
|
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. |
|
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 = |
|
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[' |
|
158 | parent=form_result['parent_group_id'], | |
173 |
owner= |
|
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(, '', '', '' |
|
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[' |
|
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 |
re |
|
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 |
re |
|
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 HasRepoGroupPermission |
|
193 | if HasRepoGroupPermissionLevel('admin')(group_name, 'group create'): | |
210 | pass |
|
194 | pass | |
211 | else: |
|
195 | else: | |
212 |
re |
|
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 |
@HasRepoGroupPermission |
|
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 HasPermissionA |
|
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(, '', '', '' |
|
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 |
re |
|
245 | raise HTTPFound(location=url('edit_repo_group', group_name=group_name)) | |
270 |
|
246 | |||
271 |
@HasRepoGroupPermission |
|
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 |
re |
|
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 |
re |
|
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(, '', '', '' |
|
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 |
re |
|
274 | raise HTTPFound(location=url('repos_group_home', group_name=gr.parent_group.group_name)) | |
307 |
re |
|
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 |
@HasRepoGroupPermission |
|
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 = RepoGroup |
|
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 |
|
|
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 |
@HasRepoGroupPermission |
|
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 = RepoGroup |
|
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 |
@HasRepoGroupPermission |
|
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 = RepoGroup |
|
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 |
@HasRepoGroupPermission |
|
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 = RepoGroup |
|
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 |
@HasRepoGroupPermission |
|
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 = RepoGroup |
|
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 |
|
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 |
re |
|
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( |
|
366 | #action_logger(request.authuser, 'admin_changed_repo_permissions', | |
418 |
# repo_name, |
|
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 |
re |
|
370 | raise HTTPFound(location=url('edit_repo_group_perms', group_name=group_name)) | |
422 |
|
371 | |||
423 |
@HasRepoGroupPermission |
|
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 |
|
382 | if not request.authuser.is_admin: | |
439 |
if obj_type == 'user' and |
|
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 |
HasRepoPermission |
|
40 | HasRepoPermissionLevelDecorator, NotAnonymous, HasPermissionAny | |
41 | HasRepoPermissionAnyDecorator |
|
41 | from kallithea.lib.base import BaseRepoController, render, jsonify | |
42 |
from kallithea.lib. |
|
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 |
|
64 | def _before(self, *args, **kwargs): | |
67 |
super(ReposController, self). |
|
65 | super(ReposController, self)._before(*args, **kwargs) | |
68 |
|
66 | |||
69 |
def _load_repo(self |
|
67 | def _load_repo(self): | |
70 |
repo_obj = |
|
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 |
re |
|
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_perm |
|
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_perm |
|
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 |
|
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( |
|
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_ |
|
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, |
|
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 |
re |
|
140 | raise HTTPFound(location=url('home')) | |
156 |
|
141 | |||
157 |
re |
|
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 |
@HasRepoPermission |
|
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( |
|
239 | action_logger(request.authuser, 'admin_updated_repo', | |
266 |
|
|
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( |
|
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 |
re |
|
258 | raise HTTPFound(location=url('edit_repo', repo_name=changed_name)) | |
286 |
|
259 | |||
287 |
@HasRepoPermission |
|
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 |
re |
|
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( |
|
279 | action_logger(request.authuser, 'admin_deleted_repo', | |
316 |
|
|
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 |
re |
|
294 | raise HTTPFound(location=url('repos_group_home', group_name=repo.group.group_name)) | |
331 |
re |
|
295 | raise HTTPFound(location=url('repos')) | |
332 |
|
296 | |||
333 |
@HasRepoPermission |
|
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 |
@HasRepoPermission |
|
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 |
@HasRepoPermission |
|
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( |
|
327 | #action_logger(request.authuser, 'admin_changed_repo_permissions', | |
373 |
# repo_name, |
|
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 |
re |
|
331 | raise HTTPFound(location=url('edit_repo_perms', repo_name=repo_name)) | |
377 |
|
332 | |||
378 |
@HasRepoPermission |
|
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 |
@HasRepoPermission |
|
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 |
re |
|
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 |
@HasRepoPermission |
|
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 |
|
|
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 |
@HasRepoPermission |
|
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 |
re |
|
403 | raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name)) | |
449 |
|
404 | |||
450 |
@HasRepoPermission |
|
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( |
|
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 |
|
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 |
re |
|
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 |
@HasRepoPermission |
|
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 |
re |
|
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 |
|
|
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 |
re |
|
478 | raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name)) | |
527 |
|
479 | |||
528 |
@HasRepoPermission |
|
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, |
|
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 |
re |
|
499 | raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name)) | |
548 |
|
500 | |||
549 |
@HasRepoPermission |
|
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, |
|
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 |
re |
|
518 | raise HTTPFound(location=url('summary_home', repo_name=repo_name)) | |
573 |
|
519 | |||
574 |
@HasRepoPermission |
|
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 |
|
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 |
re |
|
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 |
@HasRepoPermission |
|
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, |
|
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 |
re |
|
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 |
@HasRepoPermission |
|
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 |
re |
|
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 |
|
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, HasPermissionA |
|
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 |
|
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 |
|
63 | def _before(self, *args, **kwargs): | |
62 |
super(SettingsController, self). |
|
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[ |
|
81 | settings[k] = v | |
86 | return settings |
|
82 | return settings | |
87 |
|
83 | |||
88 |
@HasPermissionA |
|
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_ |
|
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_ |
|
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_ |
|
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 |
@HasPermissionA |
|
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= |
|
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. |
|
183 | for repo in Repository.query(): | |
221 | try: |
|
184 | try: | |
222 |
ScmModel().mark_for_invalidation(repo.repo_name |
|
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 |
re |
|
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 |
@HasPermissionA |
|
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 |
|
|
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 |
re |
|
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 |
@HasPermissionA |
|
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_meta |
|
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 |
|
|
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 |
re |
|
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 |
@HasPermissionA |
|
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 |
re |
|
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 |
|
|
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 |
re |
|
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 |
@HasPermissionA |
|
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_ |
|
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 |
|
|
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 |
re |
|
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 |
@HasPermissionA |
|
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 |
|
|
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 |
re |
|
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 |
@HasPermissionA |
|
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 |
@HasPermissionA |
|
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 |
|
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