Show More
@@ -1,213 +1,213 b'' | |||||
1 | # -*- coding: utf-8 -*- |
|
1 | # -*- coding: utf-8 -*- | |
2 | # 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 | |
3 | # 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 | |
4 | # the Free Software Foundation, either version 3 of the License, or |
|
4 | # the Free Software Foundation, either version 3 of the License, or | |
5 | # (at your option) any later version. |
|
5 | # (at your option) any later version. | |
6 | # |
|
6 | # | |
7 | # This program is distributed in the hope that it will be useful, |
|
7 | # This program is distributed in the hope that it will be useful, | |
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
10 | # GNU General Public License for more details. |
|
10 | # GNU General Public License for more details. | |
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 | """ | |
15 | Global configuration file for TurboGears2 specific settings in Kallithea. |
|
15 | Global configuration file for TurboGears2 specific settings in Kallithea. | |
16 |
|
16 | |||
17 | This file complements the .ini file. |
|
17 | This file complements the .ini file. | |
18 | """ |
|
18 | """ | |
19 |
|
19 | |||
20 | import logging |
|
20 | import logging | |
21 | import os |
|
21 | import os | |
22 | import platform |
|
22 | import platform | |
23 | import sys |
|
23 | import sys | |
24 |
|
24 | |||
25 | import alembic.config |
|
25 | import alembic.config | |
26 | import mercurial |
|
26 | import mercurial | |
27 | import tg |
|
27 | import tg | |
28 | from alembic.migration import MigrationContext |
|
28 | from alembic.migration import MigrationContext | |
29 | from alembic.script.base import ScriptDirectory |
|
29 | from alembic.script.base import ScriptDirectory | |
30 | from sqlalchemy import create_engine |
|
30 | from sqlalchemy import create_engine | |
31 | from tg.configuration import AppConfig |
|
31 | from tg.configuration import AppConfig | |
32 | from tg.support.converters import asbool |
|
32 | from tg.support.converters import asbool | |
33 |
|
33 | |||
34 | import kallithea.lib.locale |
|
34 | import kallithea.lib.locale | |
35 | import kallithea.model.base |
|
35 | import kallithea.model.base | |
36 | import kallithea.model.meta |
|
36 | import kallithea.model.meta | |
37 | from kallithea.lib.middleware.https_fixup import HttpsFixup |
|
37 | from kallithea.lib.middleware.https_fixup import HttpsFixup | |
38 | from kallithea.lib.middleware.permanent_repo_url import PermanentRepoUrl |
|
38 | from kallithea.lib.middleware.permanent_repo_url import PermanentRepoUrl | |
39 | from kallithea.lib.middleware.simplegit import SimpleGit |
|
39 | from kallithea.lib.middleware.simplegit import SimpleGit | |
40 | from kallithea.lib.middleware.simplehg import SimpleHg |
|
40 | from kallithea.lib.middleware.simplehg import SimpleHg | |
41 | from kallithea.lib.middleware.wrapper import RequestWrapper |
|
41 | from kallithea.lib.middleware.wrapper import RequestWrapper | |
42 | from kallithea.lib.utils import check_git_version, load_rcextensions, make_ui, set_app_settings, set_indexer_config, set_vcs_config |
|
42 | from kallithea.lib.utils import check_git_version, load_rcextensions, make_ui, set_app_settings, set_indexer_config, set_vcs_config | |
43 | from kallithea.lib.utils2 import str2bool |
|
43 | from kallithea.lib.utils2 import safe_str, str2bool | |
44 |
|
44 | |||
45 |
|
45 | |||
46 | log = logging.getLogger(__name__) |
|
46 | log = logging.getLogger(__name__) | |
47 |
|
47 | |||
48 |
|
48 | |||
49 | class KallitheaAppConfig(AppConfig): |
|
49 | class KallitheaAppConfig(AppConfig): | |
50 | # Note: AppConfig has a misleading name, as it's not the application |
|
50 | # Note: AppConfig has a misleading name, as it's not the application | |
51 | # configuration, but the application configurator. The AppConfig values are |
|
51 | # configuration, but the application configurator. The AppConfig values are | |
52 | # used as a template to create the actual configuration, which might |
|
52 | # used as a template to create the actual configuration, which might | |
53 | # overwrite or extend the one provided by the configurator template. |
|
53 | # overwrite or extend the one provided by the configurator template. | |
54 |
|
54 | |||
55 | # To make it clear, AppConfig creates the config and sets into it the same |
|
55 | # To make it clear, AppConfig creates the config and sets into it the same | |
56 | # values that AppConfig itself has. Then the values from the config file and |
|
56 | # values that AppConfig itself has. Then the values from the config file and | |
57 | # gearbox options are loaded and merged into the configuration. Then an |
|
57 | # gearbox options are loaded and merged into the configuration. Then an | |
58 | # after_init_config(conf) method of AppConfig is called for any change that |
|
58 | # after_init_config(conf) method of AppConfig is called for any change that | |
59 | # might depend on options provided by configuration files. |
|
59 | # might depend on options provided by configuration files. | |
60 |
|
60 | |||
61 | def __init__(self): |
|
61 | def __init__(self): | |
62 | super(KallitheaAppConfig, self).__init__() |
|
62 | super(KallitheaAppConfig, self).__init__() | |
63 |
|
63 | |||
64 | self['package'] = kallithea |
|
64 | self['package'] = kallithea | |
65 |
|
65 | |||
66 | self['prefer_toscawidgets2'] = False |
|
66 | self['prefer_toscawidgets2'] = False | |
67 | self['use_toscawidgets'] = False |
|
67 | self['use_toscawidgets'] = False | |
68 |
|
68 | |||
69 | self['renderers'] = [] |
|
69 | self['renderers'] = [] | |
70 |
|
70 | |||
71 | # Enable json in expose |
|
71 | # Enable json in expose | |
72 | self['renderers'].append('json') |
|
72 | self['renderers'].append('json') | |
73 |
|
73 | |||
74 | # Configure template rendering |
|
74 | # Configure template rendering | |
75 | self['renderers'].append('mako') |
|
75 | self['renderers'].append('mako') | |
76 | self['default_renderer'] = 'mako' |
|
76 | self['default_renderer'] = 'mako' | |
77 | self['use_dotted_templatenames'] = False |
|
77 | self['use_dotted_templatenames'] = False | |
78 |
|
78 | |||
79 | # Configure Sessions, store data as JSON to avoid pickle security issues |
|
79 | # Configure Sessions, store data as JSON to avoid pickle security issues | |
80 | self['session.enabled'] = True |
|
80 | self['session.enabled'] = True | |
81 | self['session.data_serializer'] = 'json' |
|
81 | self['session.data_serializer'] = 'json' | |
82 |
|
82 | |||
83 | # Configure the base SQLALchemy Setup |
|
83 | # Configure the base SQLALchemy Setup | |
84 | self['use_sqlalchemy'] = True |
|
84 | self['use_sqlalchemy'] = True | |
85 | self['model'] = kallithea.model.base |
|
85 | self['model'] = kallithea.model.base | |
86 | self['DBSession'] = kallithea.model.meta.Session |
|
86 | self['DBSession'] = kallithea.model.meta.Session | |
87 |
|
87 | |||
88 | # Configure App without an authentication backend. |
|
88 | # Configure App without an authentication backend. | |
89 | self['auth_backend'] = None |
|
89 | self['auth_backend'] = None | |
90 |
|
90 | |||
91 | # Use custom error page for these errors. By default, Turbogears2 does not add |
|
91 | # Use custom error page for these errors. By default, Turbogears2 does not add | |
92 | # 400 in this list. |
|
92 | # 400 in this list. | |
93 | # Explicitly listing all is considered more robust than appending to defaults, |
|
93 | # Explicitly listing all is considered more robust than appending to defaults, | |
94 | # in light of possible future framework changes. |
|
94 | # in light of possible future framework changes. | |
95 | self['errorpage.status_codes'] = [400, 401, 403, 404] |
|
95 | self['errorpage.status_codes'] = [400, 401, 403, 404] | |
96 |
|
96 | |||
97 | # Disable transaction manager -- currently Kallithea takes care of transactions itself |
|
97 | # Disable transaction manager -- currently Kallithea takes care of transactions itself | |
98 | self['tm.enabled'] = False |
|
98 | self['tm.enabled'] = False | |
99 |
|
99 | |||
100 | # Set the i18n source language so TG doesn't search beyond 'en' in Accept-Language. |
|
100 | # Set the i18n source language so TG doesn't search beyond 'en' in Accept-Language. | |
101 | # Don't force the default here if configuration force something else. |
|
101 | # Don't force the default here if configuration force something else. | |
102 | if not self.get('i18n.lang'): |
|
102 | if not self.get('i18n.lang'): | |
103 | self['i18n.lang'] = 'en' |
|
103 | self['i18n.lang'] = 'en' | |
104 |
|
104 | |||
105 |
|
105 | |||
106 | base_config = KallitheaAppConfig() |
|
106 | base_config = KallitheaAppConfig() | |
107 |
|
107 | |||
108 | # DebugBar, a debug toolbar for TurboGears2. |
|
108 | # DebugBar, a debug toolbar for TurboGears2. | |
109 | # (https://github.com/TurboGears/tgext.debugbar) |
|
109 | # (https://github.com/TurboGears/tgext.debugbar) | |
110 | # To enable it, install 'tgext.debugbar' and 'kajiki', and run Kallithea with |
|
110 | # To enable it, install 'tgext.debugbar' and 'kajiki', and run Kallithea with | |
111 | # 'debug = true' (not in production!) |
|
111 | # 'debug = true' (not in production!) | |
112 | # See the Kallithea documentation for more information. |
|
112 | # See the Kallithea documentation for more information. | |
113 | try: |
|
113 | try: | |
114 | from tgext.debugbar import enable_debugbar |
|
114 | from tgext.debugbar import enable_debugbar | |
115 | import kajiki # only to check its existence |
|
115 | import kajiki # only to check its existence | |
116 | except ImportError: |
|
116 | except ImportError: | |
117 | pass |
|
117 | pass | |
118 | else: |
|
118 | else: | |
119 | base_config['renderers'].append('kajiki') |
|
119 | base_config['renderers'].append('kajiki') | |
120 | enable_debugbar(base_config) |
|
120 | enable_debugbar(base_config) | |
121 |
|
121 | |||
122 |
|
122 | |||
123 | def setup_configuration(app): |
|
123 | def setup_configuration(app): | |
124 | config = app.config |
|
124 | config = app.config | |
125 |
|
125 | |||
126 | if not kallithea.lib.locale.current_locale_is_valid(): |
|
126 | if not kallithea.lib.locale.current_locale_is_valid(): | |
127 | log.error("Terminating ...") |
|
127 | log.error("Terminating ...") | |
128 | sys.exit(1) |
|
128 | sys.exit(1) | |
129 |
|
129 | |||
130 | # Mercurial sets encoding at module import time, so we have to monkey patch it |
|
130 | # Mercurial sets encoding at module import time, so we have to monkey patch it | |
131 | hgencoding = config.get('hgencoding') |
|
131 | hgencoding = config.get('hgencoding') | |
132 | if hgencoding: |
|
132 | if hgencoding: | |
133 | mercurial.encoding.encoding = hgencoding |
|
133 | mercurial.encoding.encoding = hgencoding | |
134 |
|
134 | |||
135 | if config.get('ignore_alembic_revision', False): |
|
135 | if config.get('ignore_alembic_revision', False): | |
136 | log.warn('database alembic revision checking is disabled') |
|
136 | log.warn('database alembic revision checking is disabled') | |
137 | else: |
|
137 | else: | |
138 | dbconf = config['sqlalchemy.url'] |
|
138 | dbconf = config['sqlalchemy.url'] | |
139 | alembic_cfg = alembic.config.Config() |
|
139 | alembic_cfg = alembic.config.Config() | |
140 | alembic_cfg.set_main_option('script_location', 'kallithea:alembic') |
|
140 | alembic_cfg.set_main_option('script_location', 'kallithea:alembic') | |
141 | alembic_cfg.set_main_option('sqlalchemy.url', dbconf) |
|
141 | alembic_cfg.set_main_option('sqlalchemy.url', dbconf) | |
142 | script_dir = ScriptDirectory.from_config(alembic_cfg) |
|
142 | script_dir = ScriptDirectory.from_config(alembic_cfg) | |
143 | available_heads = sorted(script_dir.get_heads()) |
|
143 | available_heads = sorted(script_dir.get_heads()) | |
144 |
|
144 | |||
145 | engine = create_engine(dbconf) |
|
145 | engine = create_engine(dbconf) | |
146 | with engine.connect() as conn: |
|
146 | with engine.connect() as conn: | |
147 | context = MigrationContext.configure(conn) |
|
147 | context = MigrationContext.configure(conn) | |
148 | current_heads = sorted(str(s) for s in context.get_current_heads()) |
|
148 | current_heads = sorted(str(s) for s in context.get_current_heads()) | |
149 | if current_heads != available_heads: |
|
149 | if current_heads != available_heads: | |
150 | log.error('Failed to run Kallithea:\n\n' |
|
150 | log.error('Failed to run Kallithea:\n\n' | |
151 | 'The database version does not match the Kallithea version.\n' |
|
151 | 'The database version does not match the Kallithea version.\n' | |
152 | 'Please read the documentation on how to upgrade or downgrade the database.\n' |
|
152 | 'Please read the documentation on how to upgrade or downgrade the database.\n' | |
153 | 'Current database version id(s): %s\n' |
|
153 | 'Current database version id(s): %s\n' | |
154 | 'Expected database version id(s): %s\n' |
|
154 | 'Expected database version id(s): %s\n' | |
155 | 'If you are a developer and you know what you are doing, you can add `ignore_alembic_revision = True` ' |
|
155 | 'If you are a developer and you know what you are doing, you can add `ignore_alembic_revision = True` ' | |
156 | 'to your .ini file to skip the check.\n' % (' '.join(current_heads), ' '.join(available_heads))) |
|
156 | 'to your .ini file to skip the check.\n' % (' '.join(current_heads), ' '.join(available_heads))) | |
157 | sys.exit(1) |
|
157 | sys.exit(1) | |
158 |
|
158 | |||
159 | # store some globals into kallithea |
|
159 | # store some globals into kallithea | |
160 | kallithea.CELERY_ON = str2bool(config.get('use_celery')) |
|
160 | kallithea.CELERY_ON = str2bool(config.get('use_celery')) | |
161 | kallithea.CELERY_EAGER = str2bool(config.get('celery.always.eager')) |
|
161 | kallithea.CELERY_EAGER = str2bool(config.get('celery.always.eager')) | |
162 | kallithea.CONFIG = config |
|
162 | kallithea.CONFIG = config | |
163 |
|
163 | |||
164 | load_rcextensions(root_path=config['here']) |
|
164 | load_rcextensions(root_path=config['here']) | |
165 |
|
165 | |||
166 | repos_path = make_ui().configitems(b'paths')[0][1] |
|
166 | repos_path = safe_str(make_ui().configitems(b'paths')[0][1]) | |
167 | config['base_path'] = repos_path |
|
167 | config['base_path'] = repos_path | |
168 | set_app_settings(config) |
|
168 | set_app_settings(config) | |
169 |
|
169 | |||
170 | instance_id = kallithea.CONFIG.get('instance_id', '*') |
|
170 | instance_id = kallithea.CONFIG.get('instance_id', '*') | |
171 | if instance_id == '*': |
|
171 | if instance_id == '*': | |
172 | instance_id = '%s-%s' % (platform.uname()[1], os.getpid()) |
|
172 | instance_id = '%s-%s' % (platform.uname()[1], os.getpid()) | |
173 | kallithea.CONFIG['instance_id'] = instance_id |
|
173 | kallithea.CONFIG['instance_id'] = instance_id | |
174 |
|
174 | |||
175 | # update kallithea.CONFIG with the meanwhile changed 'config' |
|
175 | # update kallithea.CONFIG with the meanwhile changed 'config' | |
176 | kallithea.CONFIG.update(config) |
|
176 | kallithea.CONFIG.update(config) | |
177 |
|
177 | |||
178 | # configure vcs and indexer libraries (they are supposed to be independent |
|
178 | # configure vcs and indexer libraries (they are supposed to be independent | |
179 | # as much as possible and thus avoid importing tg.config or |
|
179 | # as much as possible and thus avoid importing tg.config or | |
180 | # kallithea.CONFIG). |
|
180 | # kallithea.CONFIG). | |
181 | set_vcs_config(kallithea.CONFIG) |
|
181 | set_vcs_config(kallithea.CONFIG) | |
182 | set_indexer_config(kallithea.CONFIG) |
|
182 | set_indexer_config(kallithea.CONFIG) | |
183 |
|
183 | |||
184 | check_git_version() |
|
184 | check_git_version() | |
185 |
|
185 | |||
186 | kallithea.model.meta.Session.remove() |
|
186 | kallithea.model.meta.Session.remove() | |
187 |
|
187 | |||
188 |
|
188 | |||
189 | tg.hooks.register('configure_new_app', setup_configuration) |
|
189 | tg.hooks.register('configure_new_app', setup_configuration) | |
190 |
|
190 | |||
191 |
|
191 | |||
192 | def setup_application(app): |
|
192 | def setup_application(app): | |
193 | config = app.config |
|
193 | config = app.config | |
194 |
|
194 | |||
195 | # we want our low level middleware to get to the request ASAP. We don't |
|
195 | # we want our low level middleware to get to the request ASAP. We don't | |
196 | # need any stack middleware in them - especially no StatusCodeRedirect buffering |
|
196 | # need any stack middleware in them - especially no StatusCodeRedirect buffering | |
197 | app = SimpleHg(app, config) |
|
197 | app = SimpleHg(app, config) | |
198 | app = SimpleGit(app, config) |
|
198 | app = SimpleGit(app, config) | |
199 |
|
199 | |||
200 | # Enable https redirects based on HTTP_X_URL_SCHEME set by proxy |
|
200 | # Enable https redirects based on HTTP_X_URL_SCHEME set by proxy | |
201 | if any(asbool(config.get(x)) for x in ['https_fixup', 'force_https', 'use_htsts']): |
|
201 | if any(asbool(config.get(x)) for x in ['https_fixup', 'force_https', 'use_htsts']): | |
202 | app = HttpsFixup(app, config) |
|
202 | app = HttpsFixup(app, config) | |
203 |
|
203 | |||
204 | app = PermanentRepoUrl(app, config) |
|
204 | app = PermanentRepoUrl(app, config) | |
205 |
|
205 | |||
206 | # Optional and undocumented wrapper - gives more verbose request/response logging, but has a slight overhead |
|
206 | # Optional and undocumented wrapper - gives more verbose request/response logging, but has a slight overhead | |
207 | if str2bool(config.get('use_wsgi_wrapper')): |
|
207 | if str2bool(config.get('use_wsgi_wrapper')): | |
208 | app = RequestWrapper(app, config) |
|
208 | app = RequestWrapper(app, config) | |
209 |
|
209 | |||
210 | return app |
|
210 | return app | |
211 |
|
211 | |||
212 |
|
212 | |||
213 | tg.hooks.register('before_config', setup_application) |
|
213 | tg.hooks.register('before_config', setup_application) |
@@ -1,209 +1,210 b'' | |||||
1 | # -*- coding: utf-8 -*- |
|
1 | # -*- coding: utf-8 -*- | |
2 | # 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 | |
3 | # 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 | |
4 | # the Free Software Foundation, either version 3 of the License, or |
|
4 | # the Free Software Foundation, either version 3 of the License, or | |
5 | # (at your option) any later version. |
|
5 | # (at your option) any later version. | |
6 | # |
|
6 | # | |
7 | # This program is distributed in the hope that it will be useful, |
|
7 | # This program is distributed in the hope that it will be useful, | |
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
10 | # GNU General Public License for more details. |
|
10 | # GNU General Public License for more details. | |
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 | """ | |
15 | kallithea.controllers.home |
|
15 | kallithea.controllers.home | |
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
17 |
|
17 | |||
18 | Home controller for Kallithea |
|
18 | Home controller for Kallithea | |
19 |
|
19 | |||
20 | This file was forked by the Kallithea project in July 2014. |
|
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: |
|
21 | Original author and date, and relevant copyright and licensing information is below: | |
22 | :created_on: Feb 18, 2010 |
|
22 | :created_on: Feb 18, 2010 | |
23 | :author: marcink |
|
23 | :author: marcink | |
24 | :copyright: (c) 2013 RhodeCode GmbH, and others. |
|
24 | :copyright: (c) 2013 RhodeCode GmbH, and others. | |
25 | :license: GPLv3, see LICENSE.md for more details. |
|
25 | :license: GPLv3, see LICENSE.md for more details. | |
26 |
|
26 | |||
27 | """ |
|
27 | """ | |
28 |
|
28 | |||
29 | import logging |
|
29 | import logging | |
30 |
|
30 | |||
31 | from sqlalchemy import or_ |
|
31 | from sqlalchemy import or_ | |
32 | from tg import request |
|
32 | from tg import request | |
33 | from tg import tmpl_context as c |
|
33 | from tg import tmpl_context as c | |
34 | from tg.i18n import ugettext as _ |
|
34 | from tg.i18n import ugettext as _ | |
35 | from webob.exc import HTTPBadRequest |
|
35 | from webob.exc import HTTPBadRequest | |
36 |
|
36 | |||
37 | from kallithea.lib import helpers as h |
|
37 | from kallithea.lib import helpers as h | |
38 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired |
|
38 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired | |
39 | from kallithea.lib.base import BaseController, jsonify, render |
|
39 | from kallithea.lib.base import BaseController, jsonify, render | |
|
40 | from kallithea.lib.utils2 import safe_str | |||
40 | from kallithea.model.db import RepoGroup, Repository, User, UserGroup |
|
41 | from kallithea.model.db import RepoGroup, Repository, User, UserGroup | |
41 | from kallithea.model.repo import RepoModel |
|
42 | from kallithea.model.repo import RepoModel | |
42 | from kallithea.model.scm import UserGroupList |
|
43 | from kallithea.model.scm import UserGroupList | |
43 |
|
44 | |||
44 |
|
45 | |||
45 | log = logging.getLogger(__name__) |
|
46 | log = logging.getLogger(__name__) | |
46 |
|
47 | |||
47 |
|
48 | |||
48 | class HomeController(BaseController): |
|
49 | class HomeController(BaseController): | |
49 |
|
50 | |||
50 | def about(self): |
|
51 | def about(self): | |
51 | return render('/about.html') |
|
52 | return render('/about.html') | |
52 |
|
53 | |||
53 | @LoginRequired(allow_default_user=True) |
|
54 | @LoginRequired(allow_default_user=True) | |
54 | def index(self): |
|
55 | def index(self): | |
55 | c.group = None |
|
56 | c.group = None | |
56 |
|
57 | |||
57 | repo_groups_list = self.scm_model.get_repo_groups() |
|
58 | repo_groups_list = self.scm_model.get_repo_groups() | |
58 | repos_list = Repository.query(sorted=True).filter_by(group=None).all() |
|
59 | repos_list = Repository.query(sorted=True).filter_by(group=None).all() | |
59 |
|
60 | |||
60 | c.data = RepoModel().get_repos_as_dict(repos_list, |
|
61 | c.data = RepoModel().get_repos_as_dict(repos_list, | |
61 | repo_groups_list=repo_groups_list, |
|
62 | repo_groups_list=repo_groups_list, | |
62 | short_name=True) |
|
63 | short_name=True) | |
63 |
|
64 | |||
64 | return render('/index.html') |
|
65 | return render('/index.html') | |
65 |
|
66 | |||
66 | @LoginRequired(allow_default_user=True) |
|
67 | @LoginRequired(allow_default_user=True) | |
67 | @jsonify |
|
68 | @jsonify | |
68 | def repo_switcher_data(self): |
|
69 | def repo_switcher_data(self): | |
69 | if request.is_xhr: |
|
70 | if request.is_xhr: | |
70 | all_repos = Repository.query(sorted=True).all() |
|
71 | all_repos = Repository.query(sorted=True).all() | |
71 | repo_iter = self.scm_model.get_repos(all_repos) |
|
72 | repo_iter = self.scm_model.get_repos(all_repos) | |
72 | all_groups = RepoGroup.query(sorted=True).all() |
|
73 | all_groups = RepoGroup.query(sorted=True).all() | |
73 | repo_groups_iter = self.scm_model.get_repo_groups(all_groups) |
|
74 | repo_groups_iter = self.scm_model.get_repo_groups(all_groups) | |
74 |
|
75 | |||
75 | res = [{ |
|
76 | res = [{ | |
76 | 'text': _('Groups'), |
|
77 | 'text': _('Groups'), | |
77 | 'children': [ |
|
78 | 'children': [ | |
78 | {'id': obj.group_name, |
|
79 | {'id': obj.group_name, | |
79 | 'text': obj.group_name, |
|
80 | 'text': obj.group_name, | |
80 | 'type': 'group', |
|
81 | 'type': 'group', | |
81 | 'obj': {}} |
|
82 | 'obj': {}} | |
82 | for obj in repo_groups_iter |
|
83 | for obj in repo_groups_iter | |
83 | ], |
|
84 | ], | |
84 | }, |
|
85 | }, | |
85 | { |
|
86 | { | |
86 | 'text': _('Repositories'), |
|
87 | 'text': _('Repositories'), | |
87 | 'children': [ |
|
88 | 'children': [ | |
88 | {'id': obj.repo_name, |
|
89 | {'id': obj.repo_name, | |
89 | 'text': obj.repo_name, |
|
90 | 'text': obj.repo_name, | |
90 | 'type': 'repo', |
|
91 | 'type': 'repo', | |
91 | 'obj': obj.get_dict()} |
|
92 | 'obj': obj.get_dict()} | |
92 | for obj in repo_iter |
|
93 | for obj in repo_iter | |
93 | ], |
|
94 | ], | |
94 | }] |
|
95 | }] | |
95 |
|
96 | |||
96 | for res_dict in res: |
|
97 | for res_dict in res: | |
97 | for child in (res_dict['children']): |
|
98 | for child in (res_dict['children']): | |
98 | child['obj'].pop('_changeset_cache', None) # bytes cannot be encoded in json ... but this value isn't relevant on client side at all ... |
|
99 | child['obj'].pop('_changeset_cache', None) # bytes cannot be encoded in json ... but this value isn't relevant on client side at all ... | |
99 |
|
100 | |||
100 | data = { |
|
101 | data = { | |
101 | 'more': False, |
|
102 | 'more': False, | |
102 | 'results': res, |
|
103 | 'results': res, | |
103 | } |
|
104 | } | |
104 | return data |
|
105 | return data | |
105 |
|
106 | |||
106 | else: |
|
107 | else: | |
107 | raise HTTPBadRequest() |
|
108 | raise HTTPBadRequest() | |
108 |
|
109 | |||
109 | @LoginRequired(allow_default_user=True) |
|
110 | @LoginRequired(allow_default_user=True) | |
110 | @HasRepoPermissionLevelDecorator('read') |
|
111 | @HasRepoPermissionLevelDecorator('read') | |
111 | @jsonify |
|
112 | @jsonify | |
112 | def repo_refs_data(self, repo_name): |
|
113 | def repo_refs_data(self, repo_name): | |
113 | repo = Repository.get_by_repo_name(repo_name).scm_instance |
|
114 | repo = Repository.get_by_repo_name(repo_name).scm_instance | |
114 | res = [] |
|
115 | res = [] | |
115 | _branches = repo.branches.items() |
|
116 | _branches = repo.branches.items() | |
116 | if _branches: |
|
117 | if _branches: | |
117 | res.append({ |
|
118 | res.append({ | |
118 | 'text': _('Branch'), |
|
119 | 'text': _('Branch'), | |
119 | 'children': [{'id': rev, 'text': name, 'type': 'branch'} for name, rev in _branches] |
|
120 | 'children': [{'id': safe_str(rev), 'text': safe_str(name), 'type': 'branch'} for name, rev in _branches] | |
120 | }) |
|
121 | }) | |
121 | _closed_branches = repo.closed_branches.items() |
|
122 | _closed_branches = repo.closed_branches.items() | |
122 | if _closed_branches: |
|
123 | if _closed_branches: | |
123 | res.append({ |
|
124 | res.append({ | |
124 | 'text': _('Closed Branches'), |
|
125 | 'text': _('Closed Branches'), | |
125 | 'children': [{'id': rev, 'text': name, 'type': 'closed-branch'} for name, rev in _closed_branches] |
|
126 | 'children': [{'id': safe_str(rev), 'text': safe_str(name), 'type': 'closed-branch'} for name, rev in _closed_branches] | |
126 | }) |
|
127 | }) | |
127 | _tags = repo.tags.items() |
|
128 | _tags = repo.tags.items() | |
128 | if _tags: |
|
129 | if _tags: | |
129 | res.append({ |
|
130 | res.append({ | |
130 | 'text': _('Tag'), |
|
131 | 'text': _('Tag'), | |
131 | 'children': [{'id': rev, 'text': name, 'type': 'tag'} for name, rev in _tags] |
|
132 | 'children': [{'id': safe_str(rev), 'text': safe_str(name), 'type': 'tag'} for name, rev in _tags] | |
132 | }) |
|
133 | }) | |
133 | _bookmarks = repo.bookmarks.items() |
|
134 | _bookmarks = repo.bookmarks.items() | |
134 | if _bookmarks: |
|
135 | if _bookmarks: | |
135 | res.append({ |
|
136 | res.append({ | |
136 | 'text': _('Bookmark'), |
|
137 | 'text': _('Bookmark'), | |
137 | 'children': [{'id': rev, 'text': name, 'type': 'book'} for name, rev in _bookmarks] |
|
138 | 'children': [{'id': safe_str(rev), 'text': safe_str(name), 'type': 'book'} for name, rev in _bookmarks] | |
138 | }) |
|
139 | }) | |
139 | data = { |
|
140 | data = { | |
140 | 'more': False, |
|
141 | 'more': False, | |
141 | 'results': res |
|
142 | 'results': res | |
142 | } |
|
143 | } | |
143 | return data |
|
144 | return data | |
144 |
|
145 | |||
145 | @LoginRequired() |
|
146 | @LoginRequired() | |
146 | @jsonify |
|
147 | @jsonify | |
147 | def users_and_groups_data(self): |
|
148 | def users_and_groups_data(self): | |
148 | """ |
|
149 | """ | |
149 | Returns 'results' with a list of users and user groups. |
|
150 | Returns 'results' with a list of users and user groups. | |
150 |
|
151 | |||
151 | You can either use the 'key' GET parameter to get a user by providing |
|
152 | You can either use the 'key' GET parameter to get a user by providing | |
152 | the exact user key or you can use the 'query' parameter to |
|
153 | the exact user key or you can use the 'query' parameter to | |
153 | search for users by user key, first name and last name. |
|
154 | search for users by user key, first name and last name. | |
154 | 'types' defaults to just 'users' but can be set to 'users,groups' to |
|
155 | 'types' defaults to just 'users' but can be set to 'users,groups' to | |
155 | get both users and groups. |
|
156 | get both users and groups. | |
156 | No more than 500 results (of each kind) will be returned. |
|
157 | No more than 500 results (of each kind) will be returned. | |
157 | """ |
|
158 | """ | |
158 | types = request.GET.get('types', 'users').split(',') |
|
159 | types = request.GET.get('types', 'users').split(',') | |
159 | key = request.GET.get('key', '') |
|
160 | key = request.GET.get('key', '') | |
160 | query = request.GET.get('query', '') |
|
161 | query = request.GET.get('query', '') | |
161 | results = [] |
|
162 | results = [] | |
162 | if 'users' in types: |
|
163 | if 'users' in types: | |
163 | user_list = [] |
|
164 | user_list = [] | |
164 | if key: |
|
165 | if key: | |
165 | u = User.get_by_username(key) |
|
166 | u = User.get_by_username(key) | |
166 | if u: |
|
167 | if u: | |
167 | user_list = [u] |
|
168 | user_list = [u] | |
168 | elif query: |
|
169 | elif query: | |
169 | user_list = User.query() \ |
|
170 | user_list = User.query() \ | |
170 | .filter(User.is_default_user == False) \ |
|
171 | .filter(User.is_default_user == False) \ | |
171 | .filter(User.active == True) \ |
|
172 | .filter(User.active == True) \ | |
172 | .filter(or_( |
|
173 | .filter(or_( | |
173 | User.username.ilike("%%" + query + "%%"), |
|
174 | User.username.ilike("%%" + query + "%%"), | |
174 | User.name.ilike("%%" + query + "%%"), |
|
175 | User.name.ilike("%%" + query + "%%"), | |
175 | User.lastname.ilike("%%" + query + "%%"), |
|
176 | User.lastname.ilike("%%" + query + "%%"), | |
176 | )) \ |
|
177 | )) \ | |
177 | .order_by(User.username) \ |
|
178 | .order_by(User.username) \ | |
178 | .limit(500) \ |
|
179 | .limit(500) \ | |
179 | .all() |
|
180 | .all() | |
180 | for u in user_list: |
|
181 | for u in user_list: | |
181 | results.append({ |
|
182 | results.append({ | |
182 | 'type': 'user', |
|
183 | 'type': 'user', | |
183 | 'id': u.user_id, |
|
184 | 'id': u.user_id, | |
184 | 'nname': u.username, |
|
185 | 'nname': u.username, | |
185 | 'fname': u.name, |
|
186 | 'fname': u.name, | |
186 | 'lname': u.lastname, |
|
187 | 'lname': u.lastname, | |
187 | 'gravatar_lnk': h.gravatar_url(u.email, size=28, default='default'), |
|
188 | 'gravatar_lnk': h.gravatar_url(u.email, size=28, default='default'), | |
188 | 'gravatar_size': 14, |
|
189 | 'gravatar_size': 14, | |
189 | }) |
|
190 | }) | |
190 | if 'groups' in types: |
|
191 | if 'groups' in types: | |
191 | grp_list = [] |
|
192 | grp_list = [] | |
192 | if key: |
|
193 | if key: | |
193 | grp = UserGroup.get_by_group_name(key) |
|
194 | grp = UserGroup.get_by_group_name(key) | |
194 | if grp: |
|
195 | if grp: | |
195 | grp_list = [grp] |
|
196 | grp_list = [grp] | |
196 | elif query: |
|
197 | elif query: | |
197 | grp_list = UserGroup.query() \ |
|
198 | grp_list = UserGroup.query() \ | |
198 | .filter(UserGroup.users_group_name.ilike("%%" + query + "%%")) \ |
|
199 | .filter(UserGroup.users_group_name.ilike("%%" + query + "%%")) \ | |
199 | .filter(UserGroup.users_group_active == True) \ |
|
200 | .filter(UserGroup.users_group_active == True) \ | |
200 | .order_by(UserGroup.users_group_name) \ |
|
201 | .order_by(UserGroup.users_group_name) \ | |
201 | .limit(500) \ |
|
202 | .limit(500) \ | |
202 | .all() |
|
203 | .all() | |
203 | for g in UserGroupList(grp_list, perm_level='read'): |
|
204 | for g in UserGroupList(grp_list, perm_level='read'): | |
204 | results.append({ |
|
205 | results.append({ | |
205 | 'type': 'group', |
|
206 | 'type': 'group', | |
206 | 'id': g.users_group_id, |
|
207 | 'id': g.users_group_id, | |
207 | 'grname': g.users_group_name, |
|
208 | 'grname': g.users_group_name, | |
208 | }) |
|
209 | }) | |
209 | return dict(results=results) |
|
210 | return dict(results=results) |
@@ -1,644 +1,644 b'' | |||||
1 | # -*- coding: utf-8 -*- |
|
1 | # -*- coding: utf-8 -*- | |
2 | # 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 | |
3 | # 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 | |
4 | # the Free Software Foundation, either version 3 of the License, or |
|
4 | # the Free Software Foundation, either version 3 of the License, or | |
5 | # (at your option) any later version. |
|
5 | # (at your option) any later version. | |
6 | # |
|
6 | # | |
7 | # This program is distributed in the hope that it will be useful, |
|
7 | # This program is distributed in the hope that it will be useful, | |
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
10 | # GNU General Public License for more details. |
|
10 | # GNU General Public License for more details. | |
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 | |||
15 | """ |
|
15 | """ | |
16 | kallithea.lib.base |
|
16 | kallithea.lib.base | |
17 | ~~~~~~~~~~~~~~~~~~ |
|
17 | ~~~~~~~~~~~~~~~~~~ | |
18 |
|
18 | |||
19 | The base Controller API |
|
19 | The base Controller API | |
20 | Provides the BaseController class for subclassing. And usage in different |
|
20 | Provides the BaseController class for subclassing. And usage in different | |
21 | controllers |
|
21 | controllers | |
22 |
|
22 | |||
23 | This file was forked by the Kallithea project in July 2014. |
|
23 | This file was forked by the Kallithea project in July 2014. | |
24 | Original author and date, and relevant copyright and licensing information is below: |
|
24 | Original author and date, and relevant copyright and licensing information is below: | |
25 | :created_on: Oct 06, 2010 |
|
25 | :created_on: Oct 06, 2010 | |
26 | :author: marcink |
|
26 | :author: marcink | |
27 | :copyright: (c) 2013 RhodeCode GmbH, and others. |
|
27 | :copyright: (c) 2013 RhodeCode GmbH, and others. | |
28 | :license: GPLv3, see LICENSE.md for more details. |
|
28 | :license: GPLv3, see LICENSE.md for more details. | |
29 | """ |
|
29 | """ | |
30 |
|
30 | |||
31 | import base64 |
|
31 | import base64 | |
32 | import datetime |
|
32 | import datetime | |
33 | import logging |
|
33 | import logging | |
34 | import traceback |
|
34 | import traceback | |
35 | import warnings |
|
35 | import warnings | |
36 |
|
36 | |||
37 | import decorator |
|
37 | import decorator | |
38 | import paste.auth.basic |
|
38 | import paste.auth.basic | |
39 | import paste.httpexceptions |
|
39 | import paste.httpexceptions | |
40 | import paste.httpheaders |
|
40 | import paste.httpheaders | |
41 | import webob.exc |
|
41 | import webob.exc | |
42 | from tg import TGController, config, render_template, request, response, session |
|
42 | from tg import TGController, config, render_template, request, response, session | |
43 | from tg import tmpl_context as c |
|
43 | from tg import tmpl_context as c | |
44 | from tg.i18n import ugettext as _ |
|
44 | from tg.i18n import ugettext as _ | |
45 |
|
45 | |||
46 | from kallithea import BACKENDS, __version__ |
|
46 | from kallithea import BACKENDS, __version__ | |
47 | from kallithea.config.routing import url |
|
47 | from kallithea.config.routing import url | |
48 | from kallithea.lib import auth_modules, ext_json |
|
48 | from kallithea.lib import auth_modules, ext_json | |
49 | from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware |
|
49 | from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware | |
50 | from kallithea.lib.exceptions import UserCreationError |
|
50 | from kallithea.lib.exceptions import UserCreationError | |
51 | from kallithea.lib.utils import get_repo_slug, is_valid_repo |
|
51 | from kallithea.lib.utils import get_repo_slug, is_valid_repo | |
52 | from kallithea.lib.utils2 import AttributeDict, ascii_bytes, safe_int, safe_str, set_hook_environment, str2bool |
|
52 | from kallithea.lib.utils2 import AttributeDict, ascii_bytes, safe_int, safe_str, set_hook_environment, str2bool | |
53 | from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError |
|
53 | from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError | |
54 | from kallithea.model import meta |
|
54 | from kallithea.model import meta | |
55 | from kallithea.model.db import PullRequest, Repository, Setting, User |
|
55 | from kallithea.model.db import PullRequest, Repository, Setting, User | |
56 | from kallithea.model.scm import ScmModel |
|
56 | from kallithea.model.scm import ScmModel | |
57 |
|
57 | |||
58 |
|
58 | |||
59 | log = logging.getLogger(__name__) |
|
59 | log = logging.getLogger(__name__) | |
60 |
|
60 | |||
61 |
|
61 | |||
62 | def render(template_path): |
|
62 | def render(template_path): | |
63 | return render_template({'url': url}, 'mako', template_path) |
|
63 | return render_template({'url': url}, 'mako', template_path) | |
64 |
|
64 | |||
65 |
|
65 | |||
66 | def _filter_proxy(ip): |
|
66 | def _filter_proxy(ip): | |
67 | """ |
|
67 | """ | |
68 | HEADERS can have multiple ips inside the left-most being the original |
|
68 | HEADERS can have multiple ips inside the left-most being the original | |
69 | client, and each successive proxy that passed the request adding the IP |
|
69 | client, and each successive proxy that passed the request adding the IP | |
70 | address where it received the request from. |
|
70 | address where it received the request from. | |
71 |
|
71 | |||
72 | :param ip: |
|
72 | :param ip: | |
73 | """ |
|
73 | """ | |
74 | if ',' in ip: |
|
74 | if ',' in ip: | |
75 | _ips = ip.split(',') |
|
75 | _ips = ip.split(',') | |
76 | _first_ip = _ips[0].strip() |
|
76 | _first_ip = _ips[0].strip() | |
77 | log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip) |
|
77 | log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip) | |
78 | return _first_ip |
|
78 | return _first_ip | |
79 | return ip |
|
79 | return ip | |
80 |
|
80 | |||
81 |
|
81 | |||
82 | def _get_ip_addr(environ): |
|
82 | def _get_ip_addr(environ): | |
83 | proxy_key = 'HTTP_X_REAL_IP' |
|
83 | proxy_key = 'HTTP_X_REAL_IP' | |
84 | proxy_key2 = 'HTTP_X_FORWARDED_FOR' |
|
84 | proxy_key2 = 'HTTP_X_FORWARDED_FOR' | |
85 | def_key = 'REMOTE_ADDR' |
|
85 | def_key = 'REMOTE_ADDR' | |
86 |
|
86 | |||
87 | ip = environ.get(proxy_key) |
|
87 | ip = environ.get(proxy_key) | |
88 | if ip: |
|
88 | if ip: | |
89 | return _filter_proxy(ip) |
|
89 | return _filter_proxy(ip) | |
90 |
|
90 | |||
91 | ip = environ.get(proxy_key2) |
|
91 | ip = environ.get(proxy_key2) | |
92 | if ip: |
|
92 | if ip: | |
93 | return _filter_proxy(ip) |
|
93 | return _filter_proxy(ip) | |
94 |
|
94 | |||
95 | ip = environ.get(def_key, '0.0.0.0') |
|
95 | ip = environ.get(def_key, '0.0.0.0') | |
96 | return _filter_proxy(ip) |
|
96 | return _filter_proxy(ip) | |
97 |
|
97 | |||
98 |
|
98 | |||
99 | def get_path_info(environ): |
|
99 | def get_path_info(environ): | |
100 | """Return unicode PATH_INFO from environ ... using tg.original_request if available. |
|
100 | """Return unicode PATH_INFO from environ ... using tg.original_request if available. | |
101 | """ |
|
101 | """ | |
102 | org_req = environ.get('tg.original_request') |
|
102 | org_req = environ.get('tg.original_request') | |
103 | if org_req is not None: |
|
103 | if org_req is not None: | |
104 | environ = org_req.environ |
|
104 | environ = org_req.environ | |
105 | return safe_str(environ['PATH_INFO']) |
|
105 | return safe_str(environ['PATH_INFO']) | |
106 |
|
106 | |||
107 |
|
107 | |||
108 | def log_in_user(user, remember, is_external_auth, ip_addr): |
|
108 | def log_in_user(user, remember, is_external_auth, ip_addr): | |
109 | """ |
|
109 | """ | |
110 | Log a `User` in and update session and cookies. If `remember` is True, |
|
110 | Log a `User` in and update session and cookies. If `remember` is True, | |
111 | the session cookie is set to expire in a year; otherwise, it expires at |
|
111 | the session cookie is set to expire in a year; otherwise, it expires at | |
112 | the end of the browser session. |
|
112 | the end of the browser session. | |
113 |
|
113 | |||
114 | Returns populated `AuthUser` object. |
|
114 | Returns populated `AuthUser` object. | |
115 | """ |
|
115 | """ | |
116 | # It should not be possible to explicitly log in as the default user. |
|
116 | # It should not be possible to explicitly log in as the default user. | |
117 | assert not user.is_default_user, user |
|
117 | assert not user.is_default_user, user | |
118 |
|
118 | |||
119 | auth_user = AuthUser.make(dbuser=user, is_external_auth=is_external_auth, ip_addr=ip_addr) |
|
119 | auth_user = AuthUser.make(dbuser=user, is_external_auth=is_external_auth, ip_addr=ip_addr) | |
120 | if auth_user is None: |
|
120 | if auth_user is None: | |
121 | return None |
|
121 | return None | |
122 |
|
122 | |||
123 | user.update_lastlogin() |
|
123 | user.update_lastlogin() | |
124 | meta.Session().commit() |
|
124 | meta.Session().commit() | |
125 |
|
125 | |||
126 | # Start new session to prevent session fixation attacks. |
|
126 | # Start new session to prevent session fixation attacks. | |
127 | session.invalidate() |
|
127 | session.invalidate() | |
128 | session['authuser'] = cookie = auth_user.to_cookie() |
|
128 | session['authuser'] = cookie = auth_user.to_cookie() | |
129 |
|
129 | |||
130 | # If they want to be remembered, update the cookie. |
|
130 | # If they want to be remembered, update the cookie. | |
131 | # NOTE: Assumes that beaker defaults to browser session cookie. |
|
131 | # NOTE: Assumes that beaker defaults to browser session cookie. | |
132 | if remember: |
|
132 | if remember: | |
133 | t = datetime.datetime.now() + datetime.timedelta(days=365) |
|
133 | t = datetime.datetime.now() + datetime.timedelta(days=365) | |
134 | session._set_cookie_expires(t) |
|
134 | session._set_cookie_expires(t) | |
135 |
|
135 | |||
136 | session.save() |
|
136 | session.save() | |
137 |
|
137 | |||
138 | log.info('user %s is now authenticated and stored in ' |
|
138 | log.info('user %s is now authenticated and stored in ' | |
139 | 'session, session attrs %s', user.username, cookie) |
|
139 | 'session, session attrs %s', user.username, cookie) | |
140 |
|
140 | |||
141 | # dumps session attrs back to cookie |
|
141 | # dumps session attrs back to cookie | |
142 | session._update_cookie_out() |
|
142 | session._update_cookie_out() | |
143 |
|
143 | |||
144 | return auth_user |
|
144 | return auth_user | |
145 |
|
145 | |||
146 |
|
146 | |||
147 | class BasicAuth(paste.auth.basic.AuthBasicAuthenticator): |
|
147 | class BasicAuth(paste.auth.basic.AuthBasicAuthenticator): | |
148 |
|
148 | |||
149 | def __init__(self, realm, authfunc, auth_http_code=None): |
|
149 | def __init__(self, realm, authfunc, auth_http_code=None): | |
150 | self.realm = realm |
|
150 | self.realm = realm | |
151 | self.authfunc = authfunc |
|
151 | self.authfunc = authfunc | |
152 | self._rc_auth_http_code = auth_http_code |
|
152 | self._rc_auth_http_code = auth_http_code | |
153 |
|
153 | |||
154 | def build_authentication(self, environ): |
|
154 | def build_authentication(self, environ): | |
155 | head = paste.httpheaders.WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm) |
|
155 | head = paste.httpheaders.WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm) | |
156 | # Consume the whole body before sending a response |
|
156 | # Consume the whole body before sending a response | |
157 | try: |
|
157 | try: | |
158 | request_body_size = int(environ.get('CONTENT_LENGTH', 0)) |
|
158 | request_body_size = int(environ.get('CONTENT_LENGTH', 0)) | |
159 | except (ValueError): |
|
159 | except (ValueError): | |
160 | request_body_size = 0 |
|
160 | request_body_size = 0 | |
161 | environ['wsgi.input'].read(request_body_size) |
|
161 | environ['wsgi.input'].read(request_body_size) | |
162 | if self._rc_auth_http_code and self._rc_auth_http_code == '403': |
|
162 | if self._rc_auth_http_code and self._rc_auth_http_code == '403': | |
163 | # return 403 if alternative http return code is specified in |
|
163 | # return 403 if alternative http return code is specified in | |
164 | # Kallithea config |
|
164 | # Kallithea config | |
165 | return paste.httpexceptions.HTTPForbidden(headers=head) |
|
165 | return paste.httpexceptions.HTTPForbidden(headers=head) | |
166 | return paste.httpexceptions.HTTPUnauthorized(headers=head) |
|
166 | return paste.httpexceptions.HTTPUnauthorized(headers=head) | |
167 |
|
167 | |||
168 | def authenticate(self, environ): |
|
168 | def authenticate(self, environ): | |
169 | authorization = paste.httpheaders.AUTHORIZATION(environ) |
|
169 | authorization = paste.httpheaders.AUTHORIZATION(environ) | |
170 | if not authorization: |
|
170 | if not authorization: | |
171 | return self.build_authentication(environ) |
|
171 | return self.build_authentication(environ) | |
172 | (authmeth, auth) = authorization.split(' ', 1) |
|
172 | (authmeth, auth) = authorization.split(' ', 1) | |
173 | if 'basic' != authmeth.lower(): |
|
173 | if 'basic' != authmeth.lower(): | |
174 | return self.build_authentication(environ) |
|
174 | return self.build_authentication(environ) | |
175 | auth = base64.b64decode(auth.strip()) |
|
175 | auth = safe_str(base64.b64decode(auth.strip())) | |
176 | _parts = auth.split(':', 1) |
|
176 | _parts = auth.split(':', 1) | |
177 | if len(_parts) == 2: |
|
177 | if len(_parts) == 2: | |
178 | username, password = _parts |
|
178 | username, password = _parts | |
179 | if self.authfunc(username, password, environ) is not None: |
|
179 | if self.authfunc(username, password, environ) is not None: | |
180 | return username |
|
180 | return username | |
181 | return self.build_authentication(environ) |
|
181 | return self.build_authentication(environ) | |
182 |
|
182 | |||
183 | __call__ = authenticate |
|
183 | __call__ = authenticate | |
184 |
|
184 | |||
185 |
|
185 | |||
186 | class BaseVCSController(object): |
|
186 | class BaseVCSController(object): | |
187 | """Base controller for handling Mercurial/Git protocol requests |
|
187 | """Base controller for handling Mercurial/Git protocol requests | |
188 | (coming from a VCS client, and not a browser). |
|
188 | (coming from a VCS client, and not a browser). | |
189 | """ |
|
189 | """ | |
190 |
|
190 | |||
191 | scm_alias = None # 'hg' / 'git' |
|
191 | scm_alias = None # 'hg' / 'git' | |
192 |
|
192 | |||
193 | def __init__(self, application, config): |
|
193 | def __init__(self, application, config): | |
194 | self.application = application |
|
194 | self.application = application | |
195 | self.config = config |
|
195 | self.config = config | |
196 | # base path of repo locations |
|
196 | # base path of repo locations | |
197 | self.basepath = self.config['base_path'] |
|
197 | self.basepath = self.config['base_path'] | |
198 | # authenticate this VCS request using the authentication modules |
|
198 | # authenticate this VCS request using the authentication modules | |
199 | self.authenticate = BasicAuth('', auth_modules.authenticate, |
|
199 | self.authenticate = BasicAuth('', auth_modules.authenticate, | |
200 | config.get('auth_ret_code')) |
|
200 | config.get('auth_ret_code')) | |
201 |
|
201 | |||
202 | @classmethod |
|
202 | @classmethod | |
203 | def parse_request(cls, environ): |
|
203 | def parse_request(cls, environ): | |
204 | """If request is parsed as a request for this VCS, return a namespace with the parsed request. |
|
204 | """If request is parsed as a request for this VCS, return a namespace with the parsed request. | |
205 | If the request is unknown, return None. |
|
205 | If the request is unknown, return None. | |
206 | """ |
|
206 | """ | |
207 | raise NotImplementedError() |
|
207 | raise NotImplementedError() | |
208 |
|
208 | |||
209 | def _authorize(self, environ, action, repo_name, ip_addr): |
|
209 | def _authorize(self, environ, action, repo_name, ip_addr): | |
210 | """Authenticate and authorize user. |
|
210 | """Authenticate and authorize user. | |
211 |
|
211 | |||
212 | Since we're dealing with a VCS client and not a browser, we only |
|
212 | Since we're dealing with a VCS client and not a browser, we only | |
213 | support HTTP basic authentication, either directly via raw header |
|
213 | support HTTP basic authentication, either directly via raw header | |
214 | inspection, or by using container authentication to delegate the |
|
214 | inspection, or by using container authentication to delegate the | |
215 | authentication to the web server. |
|
215 | authentication to the web server. | |
216 |
|
216 | |||
217 | Returns (user, None) on successful authentication and authorization. |
|
217 | Returns (user, None) on successful authentication and authorization. | |
218 | Returns (None, wsgi_app) to send the wsgi_app response to the client. |
|
218 | Returns (None, wsgi_app) to send the wsgi_app response to the client. | |
219 | """ |
|
219 | """ | |
220 | # Use anonymous access if allowed for action on repo. |
|
220 | # Use anonymous access if allowed for action on repo. | |
221 | default_user = User.get_default_user(cache=True) |
|
221 | default_user = User.get_default_user(cache=True) | |
222 | default_authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr) |
|
222 | default_authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr) | |
223 | if default_authuser is None: |
|
223 | if default_authuser is None: | |
224 | log.debug('No anonymous access at all') # move on to proper user auth |
|
224 | log.debug('No anonymous access at all') # move on to proper user auth | |
225 | else: |
|
225 | else: | |
226 | if self._check_permission(action, default_authuser, repo_name): |
|
226 | if self._check_permission(action, default_authuser, repo_name): | |
227 | return default_authuser, None |
|
227 | return default_authuser, None | |
228 | log.debug('Not authorized to access this repository as anonymous user') |
|
228 | log.debug('Not authorized to access this repository as anonymous user') | |
229 |
|
229 | |||
230 | username = None |
|
230 | username = None | |
231 | #============================================================== |
|
231 | #============================================================== | |
232 | # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE |
|
232 | # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE | |
233 | # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS |
|
233 | # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS | |
234 | #============================================================== |
|
234 | #============================================================== | |
235 |
|
235 | |||
236 | # try to auth based on environ, container auth methods |
|
236 | # try to auth based on environ, container auth methods | |
237 | log.debug('Running PRE-AUTH for container based authentication') |
|
237 | log.debug('Running PRE-AUTH for container based authentication') | |
238 | pre_auth = auth_modules.authenticate('', '', environ) |
|
238 | pre_auth = auth_modules.authenticate('', '', environ) | |
239 | if pre_auth is not None and pre_auth.get('username'): |
|
239 | if pre_auth is not None and pre_auth.get('username'): | |
240 | username = pre_auth['username'] |
|
240 | username = pre_auth['username'] | |
241 | log.debug('PRE-AUTH got %s as username', username) |
|
241 | log.debug('PRE-AUTH got %s as username', username) | |
242 |
|
242 | |||
243 | # If not authenticated by the container, running basic auth |
|
243 | # If not authenticated by the container, running basic auth | |
244 | if not username: |
|
244 | if not username: | |
245 | self.authenticate.realm = self.config['realm'] |
|
245 | self.authenticate.realm = self.config['realm'] | |
246 | result = self.authenticate(environ) |
|
246 | result = self.authenticate(environ) | |
247 | if isinstance(result, str): |
|
247 | if isinstance(result, str): | |
248 | paste.httpheaders.AUTH_TYPE.update(environ, 'basic') |
|
248 | paste.httpheaders.AUTH_TYPE.update(environ, 'basic') | |
249 | paste.httpheaders.REMOTE_USER.update(environ, result) |
|
249 | paste.httpheaders.REMOTE_USER.update(environ, result) | |
250 | username = result |
|
250 | username = result | |
251 | else: |
|
251 | else: | |
252 | return None, result.wsgi_application |
|
252 | return None, result.wsgi_application | |
253 |
|
253 | |||
254 | #============================================================== |
|
254 | #============================================================== | |
255 | # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME |
|
255 | # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME | |
256 | #============================================================== |
|
256 | #============================================================== | |
257 | try: |
|
257 | try: | |
258 | user = User.get_by_username_or_email(username) |
|
258 | user = User.get_by_username_or_email(username) | |
259 | except Exception: |
|
259 | except Exception: | |
260 | log.error(traceback.format_exc()) |
|
260 | log.error(traceback.format_exc()) | |
261 | return None, webob.exc.HTTPInternalServerError() |
|
261 | return None, webob.exc.HTTPInternalServerError() | |
262 |
|
262 | |||
263 | authuser = AuthUser.make(dbuser=user, ip_addr=ip_addr) |
|
263 | authuser = AuthUser.make(dbuser=user, ip_addr=ip_addr) | |
264 | if authuser is None: |
|
264 | if authuser is None: | |
265 | return None, webob.exc.HTTPForbidden() |
|
265 | return None, webob.exc.HTTPForbidden() | |
266 | if not self._check_permission(action, authuser, repo_name): |
|
266 | if not self._check_permission(action, authuser, repo_name): | |
267 | return None, webob.exc.HTTPForbidden() |
|
267 | return None, webob.exc.HTTPForbidden() | |
268 |
|
268 | |||
269 | return user, None |
|
269 | return user, None | |
270 |
|
270 | |||
271 | def _handle_request(self, environ, start_response): |
|
271 | def _handle_request(self, environ, start_response): | |
272 | raise NotImplementedError() |
|
272 | raise NotImplementedError() | |
273 |
|
273 | |||
274 | def _check_permission(self, action, authuser, repo_name): |
|
274 | def _check_permission(self, action, authuser, repo_name): | |
275 | """ |
|
275 | """ | |
276 | Checks permissions using action (push/pull) user and repository |
|
276 | Checks permissions using action (push/pull) user and repository | |
277 | name |
|
277 | name | |
278 |
|
278 | |||
279 | :param action: 'push' or 'pull' action |
|
279 | :param action: 'push' or 'pull' action | |
280 | :param user: `User` instance |
|
280 | :param user: `User` instance | |
281 | :param repo_name: repository name |
|
281 | :param repo_name: repository name | |
282 | """ |
|
282 | """ | |
283 | if action == 'push': |
|
283 | if action == 'push': | |
284 | if not HasPermissionAnyMiddleware('repository.write', |
|
284 | if not HasPermissionAnyMiddleware('repository.write', | |
285 | 'repository.admin')(authuser, |
|
285 | 'repository.admin')(authuser, | |
286 | repo_name): |
|
286 | repo_name): | |
287 | return False |
|
287 | return False | |
288 |
|
288 | |||
289 | else: |
|
289 | else: | |
290 | #any other action need at least read permission |
|
290 | #any other action need at least read permission | |
291 | if not HasPermissionAnyMiddleware('repository.read', |
|
291 | if not HasPermissionAnyMiddleware('repository.read', | |
292 | 'repository.write', |
|
292 | 'repository.write', | |
293 | 'repository.admin')(authuser, |
|
293 | 'repository.admin')(authuser, | |
294 | repo_name): |
|
294 | repo_name): | |
295 | return False |
|
295 | return False | |
296 |
|
296 | |||
297 | return True |
|
297 | return True | |
298 |
|
298 | |||
299 | def _get_ip_addr(self, environ): |
|
299 | def _get_ip_addr(self, environ): | |
300 | return _get_ip_addr(environ) |
|
300 | return _get_ip_addr(environ) | |
301 |
|
301 | |||
302 | def __call__(self, environ, start_response): |
|
302 | def __call__(self, environ, start_response): | |
303 | try: |
|
303 | try: | |
304 | # try parsing a request for this VCS - if it fails, call the wrapped app |
|
304 | # try parsing a request for this VCS - if it fails, call the wrapped app | |
305 | parsed_request = self.parse_request(environ) |
|
305 | parsed_request = self.parse_request(environ) | |
306 | if parsed_request is None: |
|
306 | if parsed_request is None: | |
307 | return self.application(environ, start_response) |
|
307 | return self.application(environ, start_response) | |
308 |
|
308 | |||
309 | # skip passing error to error controller |
|
309 | # skip passing error to error controller | |
310 | environ['pylons.status_code_redirect'] = True |
|
310 | environ['pylons.status_code_redirect'] = True | |
311 |
|
311 | |||
312 | # quick check if repo exists... |
|
312 | # quick check if repo exists... | |
313 | if not is_valid_repo(parsed_request.repo_name, self.basepath, self.scm_alias): |
|
313 | if not is_valid_repo(parsed_request.repo_name, self.basepath, self.scm_alias): | |
314 | raise webob.exc.HTTPNotFound() |
|
314 | raise webob.exc.HTTPNotFound() | |
315 |
|
315 | |||
316 | if parsed_request.action is None: |
|
316 | if parsed_request.action is None: | |
317 | # Note: the client doesn't get the helpful error message |
|
317 | # Note: the client doesn't get the helpful error message | |
318 | raise webob.exc.HTTPBadRequest('Unable to detect pull/push action for %r! Are you using a nonstandard command or client?' % parsed_request.repo_name) |
|
318 | raise webob.exc.HTTPBadRequest('Unable to detect pull/push action for %r! Are you using a nonstandard command or client?' % parsed_request.repo_name) | |
319 |
|
319 | |||
320 | #====================================================================== |
|
320 | #====================================================================== | |
321 | # CHECK PERMISSIONS |
|
321 | # CHECK PERMISSIONS | |
322 | #====================================================================== |
|
322 | #====================================================================== | |
323 | ip_addr = self._get_ip_addr(environ) |
|
323 | ip_addr = self._get_ip_addr(environ) | |
324 | user, response_app = self._authorize(environ, parsed_request.action, parsed_request.repo_name, ip_addr) |
|
324 | user, response_app = self._authorize(environ, parsed_request.action, parsed_request.repo_name, ip_addr) | |
325 | if response_app is not None: |
|
325 | if response_app is not None: | |
326 | return response_app(environ, start_response) |
|
326 | return response_app(environ, start_response) | |
327 |
|
327 | |||
328 | #====================================================================== |
|
328 | #====================================================================== | |
329 | # REQUEST HANDLING |
|
329 | # REQUEST HANDLING | |
330 | #====================================================================== |
|
330 | #====================================================================== | |
331 | set_hook_environment(user.username, ip_addr, |
|
331 | set_hook_environment(user.username, ip_addr, | |
332 | parsed_request.repo_name, self.scm_alias, parsed_request.action) |
|
332 | parsed_request.repo_name, self.scm_alias, parsed_request.action) | |
333 |
|
333 | |||
334 | try: |
|
334 | try: | |
335 | log.info('%s action on %s repo "%s" by "%s" from %s', |
|
335 | log.info('%s action on %s repo "%s" by "%s" from %s', | |
336 | parsed_request.action, self.scm_alias, parsed_request.repo_name, user.username, ip_addr) |
|
336 | parsed_request.action, self.scm_alias, parsed_request.repo_name, user.username, ip_addr) | |
337 | app = self._make_app(parsed_request) |
|
337 | app = self._make_app(parsed_request) | |
338 | return app(environ, start_response) |
|
338 | return app(environ, start_response) | |
339 | except Exception: |
|
339 | except Exception: | |
340 | log.error(traceback.format_exc()) |
|
340 | log.error(traceback.format_exc()) | |
341 | raise webob.exc.HTTPInternalServerError() |
|
341 | raise webob.exc.HTTPInternalServerError() | |
342 |
|
342 | |||
343 | except webob.exc.HTTPException as e: |
|
343 | except webob.exc.HTTPException as e: | |
344 | return e(environ, start_response) |
|
344 | return e(environ, start_response) | |
345 |
|
345 | |||
346 |
|
346 | |||
347 | class BaseController(TGController): |
|
347 | class BaseController(TGController): | |
348 |
|
348 | |||
349 | def _before(self, *args, **kwargs): |
|
349 | def _before(self, *args, **kwargs): | |
350 | """ |
|
350 | """ | |
351 | _before is called before controller methods and after __call__ |
|
351 | _before is called before controller methods and after __call__ | |
352 | """ |
|
352 | """ | |
353 | if request.needs_csrf_check: |
|
353 | if request.needs_csrf_check: | |
354 | # CSRF protection: Whenever a request has ambient authority (whether |
|
354 | # CSRF protection: Whenever a request has ambient authority (whether | |
355 | # through a session cookie or its origin IP address), it must include |
|
355 | # through a session cookie or its origin IP address), it must include | |
356 | # the correct token, unless the HTTP method is GET or HEAD (and thus |
|
356 | # the correct token, unless the HTTP method is GET or HEAD (and thus | |
357 | # guaranteed to be side effect free. In practice, the only situation |
|
357 | # guaranteed to be side effect free. In practice, the only situation | |
358 | # where we allow side effects without ambient authority is when the |
|
358 | # where we allow side effects without ambient authority is when the | |
359 | # authority comes from an API key; and that is handled above. |
|
359 | # authority comes from an API key; and that is handled above. | |
360 | from kallithea.lib import helpers as h |
|
360 | from kallithea.lib import helpers as h | |
361 | token = request.POST.get(h.session_csrf_secret_name) |
|
361 | token = request.POST.get(h.session_csrf_secret_name) | |
362 | if not token or token != h.session_csrf_secret_token(): |
|
362 | if not token or token != h.session_csrf_secret_token(): | |
363 | log.error('CSRF check failed') |
|
363 | log.error('CSRF check failed') | |
364 | raise webob.exc.HTTPForbidden() |
|
364 | raise webob.exc.HTTPForbidden() | |
365 |
|
365 | |||
366 | c.kallithea_version = __version__ |
|
366 | c.kallithea_version = __version__ | |
367 | rc_config = Setting.get_app_settings() |
|
367 | rc_config = Setting.get_app_settings() | |
368 |
|
368 | |||
369 | # Visual options |
|
369 | # Visual options | |
370 | c.visual = AttributeDict({}) |
|
370 | c.visual = AttributeDict({}) | |
371 |
|
371 | |||
372 | ## DB stored |
|
372 | ## DB stored | |
373 | c.visual.show_public_icon = str2bool(rc_config.get('show_public_icon')) |
|
373 | c.visual.show_public_icon = str2bool(rc_config.get('show_public_icon')) | |
374 | c.visual.show_private_icon = str2bool(rc_config.get('show_private_icon')) |
|
374 | c.visual.show_private_icon = str2bool(rc_config.get('show_private_icon')) | |
375 | c.visual.stylify_metalabels = str2bool(rc_config.get('stylify_metalabels')) |
|
375 | c.visual.stylify_metalabels = str2bool(rc_config.get('stylify_metalabels')) | |
376 | c.visual.page_size = safe_int(rc_config.get('dashboard_items', 100)) |
|
376 | c.visual.page_size = safe_int(rc_config.get('dashboard_items', 100)) | |
377 | c.visual.admin_grid_items = safe_int(rc_config.get('admin_grid_items', 100)) |
|
377 | c.visual.admin_grid_items = safe_int(rc_config.get('admin_grid_items', 100)) | |
378 | c.visual.repository_fields = str2bool(rc_config.get('repository_fields')) |
|
378 | c.visual.repository_fields = str2bool(rc_config.get('repository_fields')) | |
379 | c.visual.show_version = str2bool(rc_config.get('show_version')) |
|
379 | c.visual.show_version = str2bool(rc_config.get('show_version')) | |
380 | c.visual.use_gravatar = str2bool(rc_config.get('use_gravatar')) |
|
380 | c.visual.use_gravatar = str2bool(rc_config.get('use_gravatar')) | |
381 | c.visual.gravatar_url = rc_config.get('gravatar_url') |
|
381 | c.visual.gravatar_url = rc_config.get('gravatar_url') | |
382 |
|
382 | |||
383 | c.ga_code = rc_config.get('ga_code') |
|
383 | c.ga_code = rc_config.get('ga_code') | |
384 | # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code |
|
384 | # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code | |
385 | if c.ga_code and '<' not in c.ga_code: |
|
385 | if c.ga_code and '<' not in c.ga_code: | |
386 | c.ga_code = '''<script type="text/javascript"> |
|
386 | c.ga_code = '''<script type="text/javascript"> | |
387 | var _gaq = _gaq || []; |
|
387 | var _gaq = _gaq || []; | |
388 | _gaq.push(['_setAccount', '%s']); |
|
388 | _gaq.push(['_setAccount', '%s']); | |
389 | _gaq.push(['_trackPageview']); |
|
389 | _gaq.push(['_trackPageview']); | |
390 |
|
390 | |||
391 | (function() { |
|
391 | (function() { | |
392 | var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; |
|
392 | var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; | |
393 | ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; |
|
393 | ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; | |
394 | var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); |
|
394 | var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); | |
395 | })(); |
|
395 | })(); | |
396 | </script>''' % c.ga_code |
|
396 | </script>''' % c.ga_code | |
397 | c.site_name = rc_config.get('title') |
|
397 | c.site_name = rc_config.get('title') | |
398 | c.clone_uri_tmpl = rc_config.get('clone_uri_tmpl') or Repository.DEFAULT_CLONE_URI |
|
398 | c.clone_uri_tmpl = rc_config.get('clone_uri_tmpl') or Repository.DEFAULT_CLONE_URI | |
399 | c.clone_ssh_tmpl = rc_config.get('clone_ssh_tmpl') or Repository.DEFAULT_CLONE_SSH |
|
399 | c.clone_ssh_tmpl = rc_config.get('clone_ssh_tmpl') or Repository.DEFAULT_CLONE_SSH | |
400 |
|
400 | |||
401 | ## INI stored |
|
401 | ## INI stored | |
402 | c.visual.allow_repo_location_change = str2bool(config.get('allow_repo_location_change', True)) |
|
402 | c.visual.allow_repo_location_change = str2bool(config.get('allow_repo_location_change', True)) | |
403 | c.visual.allow_custom_hooks_settings = str2bool(config.get('allow_custom_hooks_settings', True)) |
|
403 | c.visual.allow_custom_hooks_settings = str2bool(config.get('allow_custom_hooks_settings', True)) | |
404 | c.ssh_enabled = str2bool(config.get('ssh_enabled', False)) |
|
404 | c.ssh_enabled = str2bool(config.get('ssh_enabled', False)) | |
405 |
|
405 | |||
406 | c.instance_id = config.get('instance_id') |
|
406 | c.instance_id = config.get('instance_id') | |
407 | c.issues_url = config.get('bugtracker', url('issues_url')) |
|
407 | c.issues_url = config.get('bugtracker', url('issues_url')) | |
408 | # END CONFIG VARS |
|
408 | # END CONFIG VARS | |
409 |
|
409 | |||
410 | c.repo_name = get_repo_slug(request) # can be empty |
|
410 | c.repo_name = get_repo_slug(request) # can be empty | |
411 | c.backends = list(BACKENDS) |
|
411 | c.backends = list(BACKENDS) | |
412 |
|
412 | |||
413 | self.cut_off_limit = safe_int(config.get('cut_off_limit')) |
|
413 | self.cut_off_limit = safe_int(config.get('cut_off_limit')) | |
414 |
|
414 | |||
415 | c.my_pr_count = PullRequest.query(reviewer_id=request.authuser.user_id, include_closed=False).count() |
|
415 | c.my_pr_count = PullRequest.query(reviewer_id=request.authuser.user_id, include_closed=False).count() | |
416 |
|
416 | |||
417 | self.scm_model = ScmModel() |
|
417 | self.scm_model = ScmModel() | |
418 |
|
418 | |||
419 | @staticmethod |
|
419 | @staticmethod | |
420 | def _determine_auth_user(session_authuser, ip_addr): |
|
420 | def _determine_auth_user(session_authuser, ip_addr): | |
421 | """ |
|
421 | """ | |
422 | Create an `AuthUser` object given the API key/bearer token |
|
422 | Create an `AuthUser` object given the API key/bearer token | |
423 | (if any) and the value of the authuser session cookie. |
|
423 | (if any) and the value of the authuser session cookie. | |
424 | Returns None if no valid user is found (like not active or no access for IP). |
|
424 | Returns None if no valid user is found (like not active or no access for IP). | |
425 | """ |
|
425 | """ | |
426 |
|
426 | |||
427 | # Authenticate by session cookie |
|
427 | # Authenticate by session cookie | |
428 | # In ancient login sessions, 'authuser' may not be a dict. |
|
428 | # In ancient login sessions, 'authuser' may not be a dict. | |
429 | # In that case, the user will have to log in again. |
|
429 | # In that case, the user will have to log in again. | |
430 | # v0.3 and earlier included an 'is_authenticated' key; if present, |
|
430 | # v0.3 and earlier included an 'is_authenticated' key; if present, | |
431 | # this must be True. |
|
431 | # this must be True. | |
432 | if isinstance(session_authuser, dict) and session_authuser.get('is_authenticated', True): |
|
432 | if isinstance(session_authuser, dict) and session_authuser.get('is_authenticated', True): | |
433 | return AuthUser.from_cookie(session_authuser, ip_addr=ip_addr) |
|
433 | return AuthUser.from_cookie(session_authuser, ip_addr=ip_addr) | |
434 |
|
434 | |||
435 | # Authenticate by auth_container plugin (if enabled) |
|
435 | # Authenticate by auth_container plugin (if enabled) | |
436 | if any( |
|
436 | if any( | |
437 | plugin.is_container_auth |
|
437 | plugin.is_container_auth | |
438 | for plugin in auth_modules.get_auth_plugins() |
|
438 | for plugin in auth_modules.get_auth_plugins() | |
439 | ): |
|
439 | ): | |
440 | try: |
|
440 | try: | |
441 | user_info = auth_modules.authenticate('', '', request.environ) |
|
441 | user_info = auth_modules.authenticate('', '', request.environ) | |
442 | except UserCreationError as e: |
|
442 | except UserCreationError as e: | |
443 | from kallithea.lib import helpers as h |
|
443 | from kallithea.lib import helpers as h | |
444 | h.flash(e, 'error', logf=log.error) |
|
444 | h.flash(e, 'error', logf=log.error) | |
445 | else: |
|
445 | else: | |
446 | if user_info is not None: |
|
446 | if user_info is not None: | |
447 | username = user_info['username'] |
|
447 | username = user_info['username'] | |
448 | user = User.get_by_username(username, case_insensitive=True) |
|
448 | user = User.get_by_username(username, case_insensitive=True) | |
449 | return log_in_user(user, remember=False, is_external_auth=True, ip_addr=ip_addr) |
|
449 | return log_in_user(user, remember=False, is_external_auth=True, ip_addr=ip_addr) | |
450 |
|
450 | |||
451 | # User is default user (if active) or anonymous |
|
451 | # User is default user (if active) or anonymous | |
452 | default_user = User.get_default_user(cache=True) |
|
452 | default_user = User.get_default_user(cache=True) | |
453 | authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr) |
|
453 | authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr) | |
454 | if authuser is None: # fall back to anonymous |
|
454 | if authuser is None: # fall back to anonymous | |
455 | authuser = AuthUser(dbuser=default_user) # TODO: somehow use .make? |
|
455 | authuser = AuthUser(dbuser=default_user) # TODO: somehow use .make? | |
456 | return authuser |
|
456 | return authuser | |
457 |
|
457 | |||
458 | @staticmethod |
|
458 | @staticmethod | |
459 | def _basic_security_checks(): |
|
459 | def _basic_security_checks(): | |
460 | """Perform basic security/sanity checks before processing the request.""" |
|
460 | """Perform basic security/sanity checks before processing the request.""" | |
461 |
|
461 | |||
462 | # Only allow the following HTTP request methods. |
|
462 | # Only allow the following HTTP request methods. | |
463 | if request.method not in ['GET', 'HEAD', 'POST']: |
|
463 | if request.method not in ['GET', 'HEAD', 'POST']: | |
464 | raise webob.exc.HTTPMethodNotAllowed() |
|
464 | raise webob.exc.HTTPMethodNotAllowed() | |
465 |
|
465 | |||
466 | # Also verify the _method override - no longer allowed. |
|
466 | # Also verify the _method override - no longer allowed. | |
467 | if request.params.get('_method') is None: |
|
467 | if request.params.get('_method') is None: | |
468 | pass # no override, no problem |
|
468 | pass # no override, no problem | |
469 | else: |
|
469 | else: | |
470 | raise webob.exc.HTTPMethodNotAllowed() |
|
470 | raise webob.exc.HTTPMethodNotAllowed() | |
471 |
|
471 | |||
472 | # Make sure CSRF token never appears in the URL. If so, invalidate it. |
|
472 | # Make sure CSRF token never appears in the URL. If so, invalidate it. | |
473 | from kallithea.lib import helpers as h |
|
473 | from kallithea.lib import helpers as h | |
474 | if h.session_csrf_secret_name in request.GET: |
|
474 | if h.session_csrf_secret_name in request.GET: | |
475 | log.error('CSRF key leak detected') |
|
475 | log.error('CSRF key leak detected') | |
476 | session.pop(h.session_csrf_secret_name, None) |
|
476 | session.pop(h.session_csrf_secret_name, None) | |
477 | session.save() |
|
477 | session.save() | |
478 | h.flash(_('CSRF token leak has been detected - all form tokens have been expired'), |
|
478 | h.flash(_('CSRF token leak has been detected - all form tokens have been expired'), | |
479 | category='error') |
|
479 | category='error') | |
480 |
|
480 | |||
481 | # WebOb already ignores request payload parameters for anything other |
|
481 | # WebOb already ignores request payload parameters for anything other | |
482 | # than POST/PUT, but double-check since other Kallithea code relies on |
|
482 | # than POST/PUT, but double-check since other Kallithea code relies on | |
483 | # this assumption. |
|
483 | # this assumption. | |
484 | if request.method not in ['POST', 'PUT'] and request.POST: |
|
484 | if request.method not in ['POST', 'PUT'] and request.POST: | |
485 | log.error('%r request with payload parameters; WebOb should have stopped this', request.method) |
|
485 | log.error('%r request with payload parameters; WebOb should have stopped this', request.method) | |
486 | raise webob.exc.HTTPBadRequest() |
|
486 | raise webob.exc.HTTPBadRequest() | |
487 |
|
487 | |||
488 | def __call__(self, environ, context): |
|
488 | def __call__(self, environ, context): | |
489 | try: |
|
489 | try: | |
490 | ip_addr = _get_ip_addr(environ) |
|
490 | ip_addr = _get_ip_addr(environ) | |
491 | self._basic_security_checks() |
|
491 | self._basic_security_checks() | |
492 |
|
492 | |||
493 | api_key = request.GET.get('api_key') |
|
493 | api_key = request.GET.get('api_key') | |
494 | try: |
|
494 | try: | |
495 | # Request.authorization may raise ValueError on invalid input |
|
495 | # Request.authorization may raise ValueError on invalid input | |
496 | type, params = request.authorization |
|
496 | type, params = request.authorization | |
497 | except (ValueError, TypeError): |
|
497 | except (ValueError, TypeError): | |
498 | pass |
|
498 | pass | |
499 | else: |
|
499 | else: | |
500 | if type.lower() == 'bearer': |
|
500 | if type.lower() == 'bearer': | |
501 | api_key = params # bearer token is an api key too |
|
501 | api_key = params # bearer token is an api key too | |
502 |
|
502 | |||
503 | if api_key is None: |
|
503 | if api_key is None: | |
504 | authuser = self._determine_auth_user( |
|
504 | authuser = self._determine_auth_user( | |
505 | session.get('authuser'), |
|
505 | session.get('authuser'), | |
506 | ip_addr=ip_addr, |
|
506 | ip_addr=ip_addr, | |
507 | ) |
|
507 | ) | |
508 | needs_csrf_check = request.method not in ['GET', 'HEAD'] |
|
508 | needs_csrf_check = request.method not in ['GET', 'HEAD'] | |
509 |
|
509 | |||
510 | else: |
|
510 | else: | |
511 | dbuser = User.get_by_api_key(api_key) |
|
511 | dbuser = User.get_by_api_key(api_key) | |
512 | if dbuser is None: |
|
512 | if dbuser is None: | |
513 | log.info('No db user found for authentication with API key ****%s from %s', |
|
513 | log.info('No db user found for authentication with API key ****%s from %s', | |
514 | api_key[-4:], ip_addr) |
|
514 | api_key[-4:], ip_addr) | |
515 | authuser = AuthUser.make(dbuser=dbuser, is_external_auth=True, ip_addr=ip_addr) |
|
515 | authuser = AuthUser.make(dbuser=dbuser, is_external_auth=True, ip_addr=ip_addr) | |
516 | needs_csrf_check = False # API key provides CSRF protection |
|
516 | needs_csrf_check = False # API key provides CSRF protection | |
517 |
|
517 | |||
518 | if authuser is None: |
|
518 | if authuser is None: | |
519 | log.info('No valid user found') |
|
519 | log.info('No valid user found') | |
520 | raise webob.exc.HTTPForbidden() |
|
520 | raise webob.exc.HTTPForbidden() | |
521 |
|
521 | |||
522 | # set globals for auth user |
|
522 | # set globals for auth user | |
523 | request.authuser = authuser |
|
523 | request.authuser = authuser | |
524 | request.ip_addr = ip_addr |
|
524 | request.ip_addr = ip_addr | |
525 | request.needs_csrf_check = needs_csrf_check |
|
525 | request.needs_csrf_check = needs_csrf_check | |
526 |
|
526 | |||
527 | log.info('IP: %s User: %s accessed %s', |
|
527 | log.info('IP: %s User: %s accessed %s', | |
528 | request.ip_addr, request.authuser, |
|
528 | request.ip_addr, request.authuser, | |
529 | get_path_info(environ), |
|
529 | get_path_info(environ), | |
530 | ) |
|
530 | ) | |
531 | return super(BaseController, self).__call__(environ, context) |
|
531 | return super(BaseController, self).__call__(environ, context) | |
532 | except webob.exc.HTTPException as e: |
|
532 | except webob.exc.HTTPException as e: | |
533 | return e |
|
533 | return e | |
534 |
|
534 | |||
535 |
|
535 | |||
536 | class BaseRepoController(BaseController): |
|
536 | class BaseRepoController(BaseController): | |
537 | """ |
|
537 | """ | |
538 | Base class for controllers responsible for loading all needed data for |
|
538 | Base class for controllers responsible for loading all needed data for | |
539 | repository loaded items are |
|
539 | repository loaded items are | |
540 |
|
540 | |||
541 | c.db_repo_scm_instance: instance of scm repository |
|
541 | c.db_repo_scm_instance: instance of scm repository | |
542 | c.db_repo: instance of db |
|
542 | c.db_repo: instance of db | |
543 | c.repository_followers: number of followers |
|
543 | c.repository_followers: number of followers | |
544 | c.repository_forks: number of forks |
|
544 | c.repository_forks: number of forks | |
545 | c.repository_following: weather the current user is following the current repo |
|
545 | c.repository_following: weather the current user is following the current repo | |
546 | """ |
|
546 | """ | |
547 |
|
547 | |||
548 | def _before(self, *args, **kwargs): |
|
548 | def _before(self, *args, **kwargs): | |
549 | super(BaseRepoController, self)._before(*args, **kwargs) |
|
549 | super(BaseRepoController, self)._before(*args, **kwargs) | |
550 | if c.repo_name: # extracted from routes |
|
550 | if c.repo_name: # extracted from routes | |
551 | _dbr = Repository.get_by_repo_name(c.repo_name) |
|
551 | _dbr = Repository.get_by_repo_name(c.repo_name) | |
552 | if not _dbr: |
|
552 | if not _dbr: | |
553 | return |
|
553 | return | |
554 |
|
554 | |||
555 | log.debug('Found repository in database %s with state `%s`', |
|
555 | log.debug('Found repository in database %s with state `%s`', | |
556 | _dbr, _dbr.repo_state) |
|
556 | _dbr, _dbr.repo_state) | |
557 | route = getattr(request.environ.get('routes.route'), 'name', '') |
|
557 | route = getattr(request.environ.get('routes.route'), 'name', '') | |
558 |
|
558 | |||
559 | # allow to delete repos that are somehow damages in filesystem |
|
559 | # allow to delete repos that are somehow damages in filesystem | |
560 | if route in ['delete_repo']: |
|
560 | if route in ['delete_repo']: | |
561 | return |
|
561 | return | |
562 |
|
562 | |||
563 | if _dbr.repo_state in [Repository.STATE_PENDING]: |
|
563 | if _dbr.repo_state in [Repository.STATE_PENDING]: | |
564 | if route in ['repo_creating_home']: |
|
564 | if route in ['repo_creating_home']: | |
565 | return |
|
565 | return | |
566 | check_url = url('repo_creating_home', repo_name=c.repo_name) |
|
566 | check_url = url('repo_creating_home', repo_name=c.repo_name) | |
567 | raise webob.exc.HTTPFound(location=check_url) |
|
567 | raise webob.exc.HTTPFound(location=check_url) | |
568 |
|
568 | |||
569 | dbr = c.db_repo = _dbr |
|
569 | dbr = c.db_repo = _dbr | |
570 | c.db_repo_scm_instance = c.db_repo.scm_instance |
|
570 | c.db_repo_scm_instance = c.db_repo.scm_instance | |
571 | if c.db_repo_scm_instance is None: |
|
571 | if c.db_repo_scm_instance is None: | |
572 | log.error('%s this repository is present in database but it ' |
|
572 | log.error('%s this repository is present in database but it ' | |
573 | 'cannot be created as an scm instance', c.repo_name) |
|
573 | 'cannot be created as an scm instance', c.repo_name) | |
574 | from kallithea.lib import helpers as h |
|
574 | from kallithea.lib import helpers as h | |
575 | h.flash(_('Repository not found in the filesystem'), |
|
575 | h.flash(_('Repository not found in the filesystem'), | |
576 | category='error') |
|
576 | category='error') | |
577 | raise webob.exc.HTTPNotFound() |
|
577 | raise webob.exc.HTTPNotFound() | |
578 |
|
578 | |||
579 | # some globals counter for menu |
|
579 | # some globals counter for menu | |
580 | c.repository_followers = self.scm_model.get_followers(dbr) |
|
580 | c.repository_followers = self.scm_model.get_followers(dbr) | |
581 | c.repository_forks = self.scm_model.get_forks(dbr) |
|
581 | c.repository_forks = self.scm_model.get_forks(dbr) | |
582 | c.repository_pull_requests = self.scm_model.get_pull_requests(dbr) |
|
582 | c.repository_pull_requests = self.scm_model.get_pull_requests(dbr) | |
583 | c.repository_following = self.scm_model.is_following_repo( |
|
583 | c.repository_following = self.scm_model.is_following_repo( | |
584 | c.repo_name, request.authuser.user_id) |
|
584 | c.repo_name, request.authuser.user_id) | |
585 |
|
585 | |||
586 | @staticmethod |
|
586 | @staticmethod | |
587 | def _get_ref_rev(repo, ref_type, ref_name, returnempty=False): |
|
587 | def _get_ref_rev(repo, ref_type, ref_name, returnempty=False): | |
588 | """ |
|
588 | """ | |
589 | Safe way to get changeset. If error occurs show error. |
|
589 | Safe way to get changeset. If error occurs show error. | |
590 | """ |
|
590 | """ | |
591 | from kallithea.lib import helpers as h |
|
591 | from kallithea.lib import helpers as h | |
592 | try: |
|
592 | try: | |
593 | return repo.scm_instance.get_ref_revision(ref_type, ref_name) |
|
593 | return repo.scm_instance.get_ref_revision(ref_type, ref_name) | |
594 | except EmptyRepositoryError as e: |
|
594 | except EmptyRepositoryError as e: | |
595 | if returnempty: |
|
595 | if returnempty: | |
596 | return repo.scm_instance.EMPTY_CHANGESET |
|
596 | return repo.scm_instance.EMPTY_CHANGESET | |
597 | h.flash(_('There are no changesets yet'), category='error') |
|
597 | h.flash(_('There are no changesets yet'), category='error') | |
598 | raise webob.exc.HTTPNotFound() |
|
598 | raise webob.exc.HTTPNotFound() | |
599 | except ChangesetDoesNotExistError as e: |
|
599 | except ChangesetDoesNotExistError as e: | |
600 | h.flash(_('Changeset for %s %s not found in %s') % |
|
600 | h.flash(_('Changeset for %s %s not found in %s') % | |
601 | (ref_type, ref_name, repo.repo_name), |
|
601 | (ref_type, ref_name, repo.repo_name), | |
602 | category='error') |
|
602 | category='error') | |
603 | raise webob.exc.HTTPNotFound() |
|
603 | raise webob.exc.HTTPNotFound() | |
604 | except RepositoryError as e: |
|
604 | except RepositoryError as e: | |
605 | log.error(traceback.format_exc()) |
|
605 | log.error(traceback.format_exc()) | |
606 | h.flash(e, category='error') |
|
606 | h.flash(e, category='error') | |
607 | raise webob.exc.HTTPBadRequest() |
|
607 | raise webob.exc.HTTPBadRequest() | |
608 |
|
608 | |||
609 |
|
609 | |||
610 | @decorator.decorator |
|
610 | @decorator.decorator | |
611 | def jsonify(func, *args, **kwargs): |
|
611 | def jsonify(func, *args, **kwargs): | |
612 | """Action decorator that formats output for JSON |
|
612 | """Action decorator that formats output for JSON | |
613 |
|
613 | |||
614 | Given a function that will return content, this decorator will turn |
|
614 | Given a function that will return content, this decorator will turn | |
615 | the result into JSON, with a content-type of 'application/json' and |
|
615 | the result into JSON, with a content-type of 'application/json' and | |
616 | output it. |
|
616 | output it. | |
617 | """ |
|
617 | """ | |
618 | response.headers['Content-Type'] = 'application/json; charset=utf-8' |
|
618 | response.headers['Content-Type'] = 'application/json; charset=utf-8' | |
619 | data = func(*args, **kwargs) |
|
619 | data = func(*args, **kwargs) | |
620 | if isinstance(data, (list, tuple)): |
|
620 | if isinstance(data, (list, tuple)): | |
621 | # A JSON list response is syntactically valid JavaScript and can be |
|
621 | # A JSON list response is syntactically valid JavaScript and can be | |
622 | # loaded and executed as JavaScript by a malicious third-party site |
|
622 | # loaded and executed as JavaScript by a malicious third-party site | |
623 | # using <script>, which can lead to cross-site data leaks. |
|
623 | # using <script>, which can lead to cross-site data leaks. | |
624 | # JSON responses should therefore be scalars or objects (i.e. Python |
|
624 | # JSON responses should therefore be scalars or objects (i.e. Python | |
625 | # dicts), because a JSON object is a syntax error if intepreted as JS. |
|
625 | # dicts), because a JSON object is a syntax error if intepreted as JS. | |
626 | msg = "JSON responses with Array envelopes are susceptible to " \ |
|
626 | msg = "JSON responses with Array envelopes are susceptible to " \ | |
627 | "cross-site data leak attacks, see " \ |
|
627 | "cross-site data leak attacks, see " \ | |
628 | "https://web.archive.org/web/20120519231904/http://wiki.pylonshq.com/display/pylonsfaq/Warnings" |
|
628 | "https://web.archive.org/web/20120519231904/http://wiki.pylonshq.com/display/pylonsfaq/Warnings" | |
629 | warnings.warn(msg, Warning, 2) |
|
629 | warnings.warn(msg, Warning, 2) | |
630 | log.warning(msg) |
|
630 | log.warning(msg) | |
631 | log.debug("Returning JSON wrapped action output") |
|
631 | log.debug("Returning JSON wrapped action output") | |
632 | return ascii_bytes(ext_json.dumps(data)) |
|
632 | return ascii_bytes(ext_json.dumps(data)) | |
633 |
|
633 | |||
634 | @decorator.decorator |
|
634 | @decorator.decorator | |
635 | def IfSshEnabled(func, *args, **kwargs): |
|
635 | def IfSshEnabled(func, *args, **kwargs): | |
636 | """Decorator for functions that can only be called if SSH access is enabled. |
|
636 | """Decorator for functions that can only be called if SSH access is enabled. | |
637 |
|
637 | |||
638 | If SSH access is disabled in the configuration file, HTTPNotFound is raised. |
|
638 | If SSH access is disabled in the configuration file, HTTPNotFound is raised. | |
639 | """ |
|
639 | """ | |
640 | if not c.ssh_enabled: |
|
640 | if not c.ssh_enabled: | |
641 | from kallithea.lib import helpers as h |
|
641 | from kallithea.lib import helpers as h | |
642 | h.flash(_("SSH access is disabled."), category='warning') |
|
642 | h.flash(_("SSH access is disabled."), category='warning') | |
643 | raise webob.exc.HTTPNotFound() |
|
643 | raise webob.exc.HTTPNotFound() | |
644 | return func(*args, **kwargs) |
|
644 | return func(*args, **kwargs) |
@@ -1,679 +1,679 b'' | |||||
1 | # -*- coding: utf-8 -*- |
|
1 | # -*- coding: utf-8 -*- | |
2 | # 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 | |
3 | # 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 | |
4 | # the Free Software Foundation, either version 3 of the License, or |
|
4 | # the Free Software Foundation, either version 3 of the License, or | |
5 | # (at your option) any later version. |
|
5 | # (at your option) any later version. | |
6 | # |
|
6 | # | |
7 | # This program is distributed in the hope that it will be useful, |
|
7 | # This program is distributed in the hope that it will be useful, | |
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
10 | # GNU General Public License for more details. |
|
10 | # GNU General Public License for more details. | |
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 | """ | |
15 | kallithea.lib.diffs |
|
15 | kallithea.lib.diffs | |
16 | ~~~~~~~~~~~~~~~~~~~ |
|
16 | ~~~~~~~~~~~~~~~~~~~ | |
17 |
|
17 | |||
18 | Set of diffing helpers, previously part of vcs |
|
18 | Set of diffing helpers, previously part of vcs | |
19 |
|
19 | |||
20 |
|
20 | |||
21 | This file was forked by the Kallithea project in July 2014. |
|
21 | This file was forked by the Kallithea project in July 2014. | |
22 | Original author and date, and relevant copyright and licensing information is below: |
|
22 | Original author and date, and relevant copyright and licensing information is below: | |
23 | :created_on: Dec 4, 2011 |
|
23 | :created_on: Dec 4, 2011 | |
24 | :author: marcink |
|
24 | :author: marcink | |
25 | :copyright: (c) 2013 RhodeCode GmbH, and others. |
|
25 | :copyright: (c) 2013 RhodeCode GmbH, and others. | |
26 | :license: GPLv3, see LICENSE.md for more details. |
|
26 | :license: GPLv3, see LICENSE.md for more details. | |
27 | """ |
|
27 | """ | |
28 | import difflib |
|
28 | import difflib | |
29 | import logging |
|
29 | import logging | |
30 | import re |
|
30 | import re | |
31 |
|
31 | |||
32 | from tg.i18n import ugettext as _ |
|
32 | from tg.i18n import ugettext as _ | |
33 |
|
33 | |||
34 | from kallithea.lib import helpers as h |
|
34 | from kallithea.lib import helpers as h | |
35 | from kallithea.lib.utils2 import safe_str |
|
35 | from kallithea.lib.utils2 import safe_str | |
36 | from kallithea.lib.vcs.backends.base import EmptyChangeset |
|
36 | from kallithea.lib.vcs.backends.base import EmptyChangeset | |
37 | from kallithea.lib.vcs.exceptions import VCSError |
|
37 | from kallithea.lib.vcs.exceptions import VCSError | |
38 | from kallithea.lib.vcs.nodes import FileNode, SubModuleNode |
|
38 | from kallithea.lib.vcs.nodes import FileNode, SubModuleNode | |
39 |
|
39 | |||
40 |
|
40 | |||
41 | log = logging.getLogger(__name__) |
|
41 | log = logging.getLogger(__name__) | |
42 |
|
42 | |||
43 |
|
43 | |||
44 | def _safe_id(idstring): |
|
44 | def _safe_id(idstring): | |
45 | r"""Make a string safe for including in an id attribute. |
|
45 | r"""Make a string safe for including in an id attribute. | |
46 |
|
46 | |||
47 | The HTML spec says that id attributes 'must begin with |
|
47 | The HTML spec says that id attributes 'must begin with | |
48 | a letter ([A-Za-z]) and may be followed by any number |
|
48 | a letter ([A-Za-z]) and may be followed by any number | |
49 | of letters, digits ([0-9]), hyphens ("-"), underscores |
|
49 | of letters, digits ([0-9]), hyphens ("-"), underscores | |
50 | ("_"), colons (":"), and periods (".")'. These regexps |
|
50 | ("_"), colons (":"), and periods (".")'. These regexps | |
51 | are slightly over-zealous, in that they remove colons |
|
51 | are slightly over-zealous, in that they remove colons | |
52 | and periods unnecessarily. |
|
52 | and periods unnecessarily. | |
53 |
|
53 | |||
54 | Whitespace is transformed into underscores, and then |
|
54 | Whitespace is transformed into underscores, and then | |
55 | anything which is not a hyphen or a character that |
|
55 | anything which is not a hyphen or a character that | |
56 | matches \w (alphanumerics and underscore) is removed. |
|
56 | matches \w (alphanumerics and underscore) is removed. | |
57 |
|
57 | |||
58 | """ |
|
58 | """ | |
59 | # Transform all whitespace to underscore |
|
59 | # Transform all whitespace to underscore | |
60 | idstring = re.sub(r'\s', "_", idstring) |
|
60 | idstring = re.sub(r'\s', "_", idstring) | |
61 | # Remove everything that is not a hyphen or a member of \w |
|
61 | # Remove everything that is not a hyphen or a member of \w | |
62 | idstring = re.sub(r'(?!-)\W', "", idstring).lower() |
|
62 | idstring = re.sub(r'(?!-)\W', "", idstring).lower() | |
63 | return idstring |
|
63 | return idstring | |
64 |
|
64 | |||
65 |
|
65 | |||
66 | def as_html(table_class='code-difftable', line_class='line', |
|
66 | def as_html(table_class='code-difftable', line_class='line', | |
67 | old_lineno_class='lineno old', new_lineno_class='lineno new', |
|
67 | old_lineno_class='lineno old', new_lineno_class='lineno new', | |
68 | no_lineno_class='lineno', |
|
68 | no_lineno_class='lineno', | |
69 | code_class='code', enable_comments=False, parsed_lines=None): |
|
69 | code_class='code', enable_comments=False, parsed_lines=None): | |
70 | """ |
|
70 | """ | |
71 | Return given diff as html table with customized css classes |
|
71 | Return given diff as html table with customized css classes | |
72 | """ |
|
72 | """ | |
73 | def _link_to_if(condition, label, url): |
|
73 | def _link_to_if(condition, label, url): | |
74 | """ |
|
74 | """ | |
75 | Generates a link if condition is meet or just the label if not. |
|
75 | Generates a link if condition is meet or just the label if not. | |
76 | """ |
|
76 | """ | |
77 |
|
77 | |||
78 | if condition: |
|
78 | if condition: | |
79 | return '''<a href="%(url)s" data-pseudo-content="%(label)s"></a>''' % { |
|
79 | return '''<a href="%(url)s" data-pseudo-content="%(label)s"></a>''' % { | |
80 | 'url': url, |
|
80 | 'url': url, | |
81 | 'label': label |
|
81 | 'label': label | |
82 | } |
|
82 | } | |
83 | else: |
|
83 | else: | |
84 | return label |
|
84 | return label | |
85 |
|
85 | |||
86 | _html_empty = True |
|
86 | _html_empty = True | |
87 | _html = [] |
|
87 | _html = [] | |
88 | _html.append('''<table class="%(table_class)s">\n''' % { |
|
88 | _html.append('''<table class="%(table_class)s">\n''' % { | |
89 | 'table_class': table_class |
|
89 | 'table_class': table_class | |
90 | }) |
|
90 | }) | |
91 |
|
91 | |||
92 | for diff in parsed_lines: |
|
92 | for diff in parsed_lines: | |
93 | for line in diff['chunks']: |
|
93 | for line in diff['chunks']: | |
94 | _html_empty = False |
|
94 | _html_empty = False | |
95 | for change in line: |
|
95 | for change in line: | |
96 | _html.append('''<tr class="%(lc)s %(action)s">\n''' % { |
|
96 | _html.append('''<tr class="%(lc)s %(action)s">\n''' % { | |
97 | 'lc': line_class, |
|
97 | 'lc': line_class, | |
98 | 'action': change['action'] |
|
98 | 'action': change['action'] | |
99 | }) |
|
99 | }) | |
100 | anchor_old_id = '' |
|
100 | anchor_old_id = '' | |
101 | anchor_new_id = '' |
|
101 | anchor_new_id = '' | |
102 | anchor_old = "%(filename)s_o%(oldline_no)s" % { |
|
102 | anchor_old = "%(filename)s_o%(oldline_no)s" % { | |
103 | 'filename': _safe_id(diff['filename']), |
|
103 | 'filename': _safe_id(diff['filename']), | |
104 | 'oldline_no': change['old_lineno'] |
|
104 | 'oldline_no': change['old_lineno'] | |
105 | } |
|
105 | } | |
106 | anchor_new = "%(filename)s_n%(oldline_no)s" % { |
|
106 | anchor_new = "%(filename)s_n%(oldline_no)s" % { | |
107 | 'filename': _safe_id(diff['filename']), |
|
107 | 'filename': _safe_id(diff['filename']), | |
108 | 'oldline_no': change['new_lineno'] |
|
108 | 'oldline_no': change['new_lineno'] | |
109 | } |
|
109 | } | |
110 | cond_old = (change['old_lineno'] != '...' and |
|
110 | cond_old = (change['old_lineno'] != '...' and | |
111 | change['old_lineno']) |
|
111 | change['old_lineno']) | |
112 | cond_new = (change['new_lineno'] != '...' and |
|
112 | cond_new = (change['new_lineno'] != '...' and | |
113 | change['new_lineno']) |
|
113 | change['new_lineno']) | |
114 | no_lineno = (change['old_lineno'] == '...' and |
|
114 | no_lineno = (change['old_lineno'] == '...' and | |
115 | change['new_lineno'] == '...') |
|
115 | change['new_lineno'] == '...') | |
116 | if cond_old: |
|
116 | if cond_old: | |
117 | anchor_old_id = 'id="%s"' % anchor_old |
|
117 | anchor_old_id = 'id="%s"' % anchor_old | |
118 | if cond_new: |
|
118 | if cond_new: | |
119 | anchor_new_id = 'id="%s"' % anchor_new |
|
119 | anchor_new_id = 'id="%s"' % anchor_new | |
120 | ########################################################### |
|
120 | ########################################################### | |
121 | # OLD LINE NUMBER |
|
121 | # OLD LINE NUMBER | |
122 | ########################################################### |
|
122 | ########################################################### | |
123 | _html.append('''\t<td %(a_id)s class="%(olc)s" %(colspan)s>''' % { |
|
123 | _html.append('''\t<td %(a_id)s class="%(olc)s" %(colspan)s>''' % { | |
124 | 'a_id': anchor_old_id, |
|
124 | 'a_id': anchor_old_id, | |
125 | 'olc': no_lineno_class if no_lineno else old_lineno_class, |
|
125 | 'olc': no_lineno_class if no_lineno else old_lineno_class, | |
126 | 'colspan': 'colspan="2"' if no_lineno else '' |
|
126 | 'colspan': 'colspan="2"' if no_lineno else '' | |
127 | }) |
|
127 | }) | |
128 |
|
128 | |||
129 | _html.append('''%(link)s''' % { |
|
129 | _html.append('''%(link)s''' % { | |
130 | 'link': _link_to_if(not no_lineno, change['old_lineno'], |
|
130 | 'link': _link_to_if(not no_lineno, change['old_lineno'], | |
131 | '#%s' % anchor_old) |
|
131 | '#%s' % anchor_old) | |
132 | }) |
|
132 | }) | |
133 | _html.append('''</td>\n''') |
|
133 | _html.append('''</td>\n''') | |
134 | ########################################################### |
|
134 | ########################################################### | |
135 | # NEW LINE NUMBER |
|
135 | # NEW LINE NUMBER | |
136 | ########################################################### |
|
136 | ########################################################### | |
137 |
|
137 | |||
138 | if not no_lineno: |
|
138 | if not no_lineno: | |
139 | _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % { |
|
139 | _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % { | |
140 | 'a_id': anchor_new_id, |
|
140 | 'a_id': anchor_new_id, | |
141 | 'nlc': new_lineno_class |
|
141 | 'nlc': new_lineno_class | |
142 | }) |
|
142 | }) | |
143 |
|
143 | |||
144 | _html.append('''%(link)s''' % { |
|
144 | _html.append('''%(link)s''' % { | |
145 | 'link': _link_to_if(True, change['new_lineno'], |
|
145 | 'link': _link_to_if(True, change['new_lineno'], | |
146 | '#%s' % anchor_new) |
|
146 | '#%s' % anchor_new) | |
147 | }) |
|
147 | }) | |
148 | _html.append('''</td>\n''') |
|
148 | _html.append('''</td>\n''') | |
149 | ########################################################### |
|
149 | ########################################################### | |
150 | # CODE |
|
150 | # CODE | |
151 | ########################################################### |
|
151 | ########################################################### | |
152 | comments = '' if enable_comments else 'no-comment' |
|
152 | comments = '' if enable_comments else 'no-comment' | |
153 | _html.append('''\t<td class="%(cc)s %(inc)s">''' % { |
|
153 | _html.append('''\t<td class="%(cc)s %(inc)s">''' % { | |
154 | 'cc': code_class, |
|
154 | 'cc': code_class, | |
155 | 'inc': comments |
|
155 | 'inc': comments | |
156 | }) |
|
156 | }) | |
157 | _html.append('''\n\t\t<div class="add-bubble"><div> </div></div><pre>%(code)s</pre>\n''' % { |
|
157 | _html.append('''\n\t\t<div class="add-bubble"><div> </div></div><pre>%(code)s</pre>\n''' % { | |
158 | 'code': change['line'] |
|
158 | 'code': change['line'] | |
159 | }) |
|
159 | }) | |
160 |
|
160 | |||
161 | _html.append('''\t</td>''') |
|
161 | _html.append('''\t</td>''') | |
162 | _html.append('''\n</tr>\n''') |
|
162 | _html.append('''\n</tr>\n''') | |
163 | _html.append('''</table>''') |
|
163 | _html.append('''</table>''') | |
164 | if _html_empty: |
|
164 | if _html_empty: | |
165 | return None |
|
165 | return None | |
166 | return ''.join(_html) |
|
166 | return ''.join(_html) | |
167 |
|
167 | |||
168 |
|
168 | |||
169 | def wrap_to_table(html): |
|
169 | def wrap_to_table(html): | |
170 | """Given a string with html, return it wrapped in a table, similar to what |
|
170 | """Given a string with html, return it wrapped in a table, similar to what | |
171 | DiffProcessor returns.""" |
|
171 | DiffProcessor returns.""" | |
172 | return '''\ |
|
172 | return '''\ | |
173 | <table class="code-difftable"> |
|
173 | <table class="code-difftable"> | |
174 | <tr class="line no-comment"> |
|
174 | <tr class="line no-comment"> | |
175 | <td class="lineno new"></td> |
|
175 | <td class="lineno new"></td> | |
176 | <td class="code no-comment"><pre>%s</pre></td> |
|
176 | <td class="code no-comment"><pre>%s</pre></td> | |
177 | </tr> |
|
177 | </tr> | |
178 | </table>''' % html |
|
178 | </table>''' % html | |
179 |
|
179 | |||
180 |
|
180 | |||
181 | def wrapped_diff(filenode_old, filenode_new, diff_limit=None, |
|
181 | def wrapped_diff(filenode_old, filenode_new, diff_limit=None, | |
182 | ignore_whitespace=True, line_context=3, |
|
182 | ignore_whitespace=True, line_context=3, | |
183 | enable_comments=False): |
|
183 | enable_comments=False): | |
184 | """ |
|
184 | """ | |
185 | Returns a file diff wrapped into a table. |
|
185 | Returns a file diff wrapped into a table. | |
186 | Checks for diff_limit and presents a message if the diff is too big. |
|
186 | Checks for diff_limit and presents a message if the diff is too big. | |
187 | """ |
|
187 | """ | |
188 | if filenode_old is None: |
|
188 | if filenode_old is None: | |
189 | filenode_old = FileNode(filenode_new.path, '', EmptyChangeset()) |
|
189 | filenode_old = FileNode(filenode_new.path, '', EmptyChangeset()) | |
190 |
|
190 | |||
191 | op = None |
|
191 | op = None | |
192 | a_path = filenode_old.path # default, might be overriden by actual rename in diff |
|
192 | a_path = filenode_old.path # default, might be overriden by actual rename in diff | |
193 | if filenode_old.is_binary or filenode_new.is_binary: |
|
193 | if filenode_old.is_binary or filenode_new.is_binary: | |
194 | html_diff = wrap_to_table(_('Binary file')) |
|
194 | html_diff = wrap_to_table(_('Binary file')) | |
195 | stats = (0, 0) |
|
195 | stats = (0, 0) | |
196 |
|
196 | |||
197 | elif diff_limit != -1 and ( |
|
197 | elif diff_limit != -1 and ( | |
198 | diff_limit is None or |
|
198 | diff_limit is None or | |
199 | (filenode_old.size < diff_limit and filenode_new.size < diff_limit)): |
|
199 | (filenode_old.size < diff_limit and filenode_new.size < diff_limit)): | |
200 |
|
200 | |||
201 | raw_diff = get_gitdiff(filenode_old, filenode_new, |
|
201 | raw_diff = get_gitdiff(filenode_old, filenode_new, | |
202 | ignore_whitespace=ignore_whitespace, |
|
202 | ignore_whitespace=ignore_whitespace, | |
203 | context=line_context) |
|
203 | context=line_context) | |
204 | diff_processor = DiffProcessor(raw_diff) |
|
204 | diff_processor = DiffProcessor(raw_diff) | |
205 | if diff_processor.parsed: # there should be exactly one element, for the specified file |
|
205 | if diff_processor.parsed: # there should be exactly one element, for the specified file | |
206 | f = diff_processor.parsed[0] |
|
206 | f = diff_processor.parsed[0] | |
207 | op = f['operation'] |
|
207 | op = f['operation'] | |
208 | a_path = f['old_filename'] |
|
208 | a_path = f['old_filename'] | |
209 |
|
209 | |||
210 | html_diff = as_html(parsed_lines=diff_processor.parsed, enable_comments=enable_comments) |
|
210 | html_diff = as_html(parsed_lines=diff_processor.parsed, enable_comments=enable_comments) | |
211 | stats = diff_processor.stat() |
|
211 | stats = diff_processor.stat() | |
212 |
|
212 | |||
213 | else: |
|
213 | else: | |
214 | html_diff = wrap_to_table(_('Changeset was too big and was cut off, use ' |
|
214 | html_diff = wrap_to_table(_('Changeset was too big and was cut off, use ' | |
215 | 'diff menu to display this diff')) |
|
215 | 'diff menu to display this diff')) | |
216 | stats = (0, 0) |
|
216 | stats = (0, 0) | |
217 |
|
217 | |||
218 | if not html_diff: |
|
218 | if not html_diff: | |
219 | submodules = [o for o in [filenode_new, filenode_old] if isinstance(o, SubModuleNode)] |
|
219 | submodules = [o for o in [filenode_new, filenode_old] if isinstance(o, SubModuleNode)] | |
220 | if submodules: |
|
220 | if submodules: | |
221 | html_diff = wrap_to_table(h.escape('Submodule %r' % submodules[0])) |
|
221 | html_diff = wrap_to_table(h.escape('Submodule %r' % submodules[0])) | |
222 | else: |
|
222 | else: | |
223 | html_diff = wrap_to_table(_('No changes detected')) |
|
223 | html_diff = wrap_to_table(_('No changes detected')) | |
224 |
|
224 | |||
225 | cs1 = filenode_old.changeset.raw_id |
|
225 | cs1 = filenode_old.changeset.raw_id | |
226 | cs2 = filenode_new.changeset.raw_id |
|
226 | cs2 = filenode_new.changeset.raw_id | |
227 |
|
227 | |||
228 | return cs1, cs2, a_path, html_diff, stats, op |
|
228 | return cs1, cs2, a_path, html_diff, stats, op | |
229 |
|
229 | |||
230 |
|
230 | |||
231 | def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3): |
|
231 | def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3): | |
232 | """ |
|
232 | """ | |
233 | Returns git style diff between given ``filenode_old`` and ``filenode_new``. |
|
233 | Returns git style diff between given ``filenode_old`` and ``filenode_new``. | |
234 | """ |
|
234 | """ | |
235 | # make sure we pass in default context |
|
235 | # make sure we pass in default context | |
236 | context = context or 3 |
|
236 | context = context or 3 | |
237 | submodules = [o for o in [filenode_new, filenode_old] if isinstance(o, SubModuleNode)] |
|
237 | submodules = [o for o in [filenode_new, filenode_old] if isinstance(o, SubModuleNode)] | |
238 | if submodules: |
|
238 | if submodules: | |
239 | return b'' |
|
239 | return b'' | |
240 |
|
240 | |||
241 | for filenode in (filenode_old, filenode_new): |
|
241 | for filenode in (filenode_old, filenode_new): | |
242 | if not isinstance(filenode, FileNode): |
|
242 | if not isinstance(filenode, FileNode): | |
243 | raise VCSError("Given object should be FileNode object, not %s" |
|
243 | raise VCSError("Given object should be FileNode object, not %s" | |
244 | % filenode.__class__) |
|
244 | % filenode.__class__) | |
245 |
|
245 | |||
246 | repo = filenode_new.changeset.repository |
|
246 | repo = filenode_new.changeset.repository | |
247 | old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET) |
|
247 | old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET) | |
248 | new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET) |
|
248 | new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET) | |
249 |
|
249 | |||
250 | vcs_gitdiff = get_diff(repo, old_raw_id, new_raw_id, filenode_new.path, |
|
250 | vcs_gitdiff = get_diff(repo, old_raw_id, new_raw_id, filenode_new.path, | |
251 | ignore_whitespace, context) |
|
251 | ignore_whitespace, context) | |
252 | return vcs_gitdiff |
|
252 | return vcs_gitdiff | |
253 |
|
253 | |||
254 |
|
254 | |||
255 | def get_diff(scm_instance, rev1, rev2, path=None, ignore_whitespace=False, context=3): |
|
255 | def get_diff(scm_instance, rev1, rev2, path=None, ignore_whitespace=False, context=3): | |
256 | """ |
|
256 | """ | |
257 | A thin wrapper around vcs lib get_diff. |
|
257 | A thin wrapper around vcs lib get_diff. | |
258 | """ |
|
258 | """ | |
259 | try: |
|
259 | try: | |
260 | return scm_instance.get_diff(rev1, rev2, path=path, |
|
260 | return scm_instance.get_diff(rev1, rev2, path=path, | |
261 | ignore_whitespace=ignore_whitespace, context=context) |
|
261 | ignore_whitespace=ignore_whitespace, context=context) | |
262 | except MemoryError: |
|
262 | except MemoryError: | |
263 | h.flash('MemoryError: Diff is too big', category='error') |
|
263 | h.flash('MemoryError: Diff is too big', category='error') | |
264 | return b'' |
|
264 | return b'' | |
265 |
|
265 | |||
266 |
|
266 | |||
267 | NEW_FILENODE = 1 |
|
267 | NEW_FILENODE = 1 | |
268 | DEL_FILENODE = 2 |
|
268 | DEL_FILENODE = 2 | |
269 | MOD_FILENODE = 3 |
|
269 | MOD_FILENODE = 3 | |
270 | RENAMED_FILENODE = 4 |
|
270 | RENAMED_FILENODE = 4 | |
271 | COPIED_FILENODE = 5 |
|
271 | COPIED_FILENODE = 5 | |
272 | CHMOD_FILENODE = 6 |
|
272 | CHMOD_FILENODE = 6 | |
273 | BIN_FILENODE = 7 |
|
273 | BIN_FILENODE = 7 | |
274 |
|
274 | |||
275 |
|
275 | |||
276 | class DiffProcessor(object): |
|
276 | class DiffProcessor(object): | |
277 | """ |
|
277 | """ | |
278 | Give it a unified or git diff and it returns a list of the files that were |
|
278 | Give it a unified or git diff and it returns a list of the files that were | |
279 | mentioned in the diff together with a dict of meta information that |
|
279 | mentioned in the diff together with a dict of meta information that | |
280 | can be used to render it in a HTML template. |
|
280 | can be used to render it in a HTML template. | |
281 | """ |
|
281 | """ | |
282 | _diff_git_re = re.compile(b'^diff --git', re.MULTILINE) |
|
282 | _diff_git_re = re.compile(b'^diff --git', re.MULTILINE) | |
283 |
|
283 | |||
284 | def __init__(self, diff, vcs='hg', diff_limit=None, inline_diff=True): |
|
284 | def __init__(self, diff, vcs='hg', diff_limit=None, inline_diff=True): | |
285 | """ |
|
285 | """ | |
286 | :param diff: a text in diff format |
|
286 | :param diff: a text in diff format | |
287 | :param vcs: type of version control hg or git |
|
287 | :param vcs: type of version control hg or git | |
288 | :param diff_limit: define the size of diff that is considered "big" |
|
288 | :param diff_limit: define the size of diff that is considered "big" | |
289 | based on that parameter cut off will be triggered, set to None |
|
289 | based on that parameter cut off will be triggered, set to None | |
290 | to show full diff |
|
290 | to show full diff | |
291 | """ |
|
291 | """ | |
292 | if not isinstance(diff, bytes): |
|
292 | if not isinstance(diff, bytes): | |
293 | raise Exception('Diff must be bytes - got %s' % type(diff)) |
|
293 | raise Exception('Diff must be bytes - got %s' % type(diff)) | |
294 |
|
294 | |||
295 | self._diff = memoryview(diff) |
|
295 | self._diff = memoryview(diff) | |
296 | self.adds = 0 |
|
296 | self.adds = 0 | |
297 | self.removes = 0 |
|
297 | self.removes = 0 | |
298 | self.diff_limit = diff_limit |
|
298 | self.diff_limit = diff_limit | |
299 | self.limited_diff = False |
|
299 | self.limited_diff = False | |
300 | self.vcs = vcs |
|
300 | self.vcs = vcs | |
301 | self.parsed = self._parse_gitdiff(inline_diff=inline_diff) |
|
301 | self.parsed = self._parse_gitdiff(inline_diff=inline_diff) | |
302 |
|
302 | |||
303 | def _parse_gitdiff(self, inline_diff): |
|
303 | def _parse_gitdiff(self, inline_diff): | |
304 | """Parse self._diff and return a list of dicts with meta info and chunks for each file. |
|
304 | """Parse self._diff and return a list of dicts with meta info and chunks for each file. | |
305 | Might set limited_diff. |
|
305 | Might set limited_diff. | |
306 | Optionally, do an extra pass and to extra markup of one-liner changes. |
|
306 | Optionally, do an extra pass and to extra markup of one-liner changes. | |
307 | """ |
|
307 | """ | |
308 | _files = [] # list of dicts with meta info and chunks |
|
308 | _files = [] # list of dicts with meta info and chunks | |
309 |
|
309 | |||
310 | starts = [m.start() for m in self._diff_git_re.finditer(self._diff)] |
|
310 | starts = [m.start() for m in self._diff_git_re.finditer(self._diff)] | |
311 | starts.append(len(self._diff)) |
|
311 | starts.append(len(self._diff)) | |
312 |
|
312 | |||
313 | for start, end in zip(starts, starts[1:]): |
|
313 | for start, end in zip(starts, starts[1:]): | |
314 | if self.diff_limit and end > self.diff_limit: |
|
314 | if self.diff_limit and end > self.diff_limit: | |
315 | self.limited_diff = True |
|
315 | self.limited_diff = True | |
316 | continue |
|
316 | continue | |
317 |
|
317 | |||
318 | head, diff_lines = _get_header(self.vcs, self._diff[start:end]) |
|
318 | head, diff_lines = _get_header(self.vcs, self._diff[start:end]) | |
319 |
|
319 | |||
320 | op = None |
|
320 | op = None | |
321 | stats = { |
|
321 | stats = { | |
322 | 'added': 0, |
|
322 | 'added': 0, | |
323 | 'deleted': 0, |
|
323 | 'deleted': 0, | |
324 | 'binary': False, |
|
324 | 'binary': False, | |
325 | 'ops': {}, |
|
325 | 'ops': {}, | |
326 | } |
|
326 | } | |
327 |
|
327 | |||
328 | if head['deleted_file_mode']: |
|
328 | if head['deleted_file_mode']: | |
329 | op = 'removed' |
|
329 | op = 'removed' | |
330 | stats['binary'] = True |
|
330 | stats['binary'] = True | |
331 | stats['ops'][DEL_FILENODE] = 'deleted file' |
|
331 | stats['ops'][DEL_FILENODE] = 'deleted file' | |
332 |
|
332 | |||
333 | elif head['new_file_mode']: |
|
333 | elif head['new_file_mode']: | |
334 | op = 'added' |
|
334 | op = 'added' | |
335 | stats['binary'] = True |
|
335 | stats['binary'] = True | |
336 | stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode'] |
|
336 | stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode'] | |
337 | else: # modify operation, can be cp, rename, chmod |
|
337 | else: # modify operation, can be cp, rename, chmod | |
338 | # CHMOD |
|
338 | # CHMOD | |
339 | if head['new_mode'] and head['old_mode']: |
|
339 | if head['new_mode'] and head['old_mode']: | |
340 | op = 'modified' |
|
340 | op = 'modified' | |
341 | stats['binary'] = True |
|
341 | stats['binary'] = True | |
342 | stats['ops'][CHMOD_FILENODE] = ('modified file chmod %s => %s' |
|
342 | stats['ops'][CHMOD_FILENODE] = ('modified file chmod %s => %s' | |
343 | % (head['old_mode'], head['new_mode'])) |
|
343 | % (head['old_mode'], head['new_mode'])) | |
344 | # RENAME |
|
344 | # RENAME | |
345 | if (head['rename_from'] and head['rename_to'] |
|
345 | if (head['rename_from'] and head['rename_to'] | |
346 | and head['rename_from'] != head['rename_to']): |
|
346 | and head['rename_from'] != head['rename_to']): | |
347 | op = 'renamed' |
|
347 | op = 'renamed' | |
348 | stats['binary'] = True |
|
348 | stats['binary'] = True | |
349 | stats['ops'][RENAMED_FILENODE] = ('file renamed from %s to %s' |
|
349 | stats['ops'][RENAMED_FILENODE] = ('file renamed from %s to %s' | |
350 | % (head['rename_from'], head['rename_to'])) |
|
350 | % (head['rename_from'], head['rename_to'])) | |
351 | # COPY |
|
351 | # COPY | |
352 | if head.get('copy_from') and head.get('copy_to'): |
|
352 | if head.get('copy_from') and head.get('copy_to'): | |
353 | op = 'modified' |
|
353 | op = 'modified' | |
354 | stats['binary'] = True |
|
354 | stats['binary'] = True | |
355 | stats['ops'][COPIED_FILENODE] = ('file copied from %s to %s' |
|
355 | stats['ops'][COPIED_FILENODE] = ('file copied from %s to %s' | |
356 | % (head['copy_from'], head['copy_to'])) |
|
356 | % (head['copy_from'], head['copy_to'])) | |
357 | # FALL BACK: detect missed old style add or remove |
|
357 | # FALL BACK: detect missed old style add or remove | |
358 | if op is None: |
|
358 | if op is None: | |
359 | if not head['a_file'] and head['b_file']: |
|
359 | if not head['a_file'] and head['b_file']: | |
360 | op = 'added' |
|
360 | op = 'added' | |
361 | stats['binary'] = True |
|
361 | stats['binary'] = True | |
362 | stats['ops'][NEW_FILENODE] = 'new file' |
|
362 | stats['ops'][NEW_FILENODE] = 'new file' | |
363 |
|
363 | |||
364 | elif head['a_file'] and not head['b_file']: |
|
364 | elif head['a_file'] and not head['b_file']: | |
365 | op = 'removed' |
|
365 | op = 'removed' | |
366 | stats['binary'] = True |
|
366 | stats['binary'] = True | |
367 | stats['ops'][DEL_FILENODE] = 'deleted file' |
|
367 | stats['ops'][DEL_FILENODE] = 'deleted file' | |
368 |
|
368 | |||
369 | # it's not ADD not DELETE |
|
369 | # it's not ADD not DELETE | |
370 | if op is None: |
|
370 | if op is None: | |
371 | op = 'modified' |
|
371 | op = 'modified' | |
372 | stats['binary'] = True |
|
372 | stats['binary'] = True | |
373 | stats['ops'][MOD_FILENODE] = 'modified file' |
|
373 | stats['ops'][MOD_FILENODE] = 'modified file' | |
374 |
|
374 | |||
375 | # a real non-binary diff |
|
375 | # a real non-binary diff | |
376 | if head['a_file'] or head['b_file']: |
|
376 | if head['a_file'] or head['b_file']: | |
377 | chunks, added, deleted = _parse_lines(diff_lines) |
|
377 | chunks, added, deleted = _parse_lines(diff_lines) | |
378 | stats['binary'] = False |
|
378 | stats['binary'] = False | |
379 | stats['added'] = added |
|
379 | stats['added'] = added | |
380 | stats['deleted'] = deleted |
|
380 | stats['deleted'] = deleted | |
381 | # explicit mark that it's a modified file |
|
381 | # explicit mark that it's a modified file | |
382 | if op == 'modified': |
|
382 | if op == 'modified': | |
383 | stats['ops'][MOD_FILENODE] = 'modified file' |
|
383 | stats['ops'][MOD_FILENODE] = 'modified file' | |
384 | else: # Git binary patch (or empty diff) |
|
384 | else: # Git binary patch (or empty diff) | |
385 | # Git binary patch |
|
385 | # Git binary patch | |
386 | if head['bin_patch']: |
|
386 | if head['bin_patch']: | |
387 | stats['ops'][BIN_FILENODE] = 'binary diff not shown' |
|
387 | stats['ops'][BIN_FILENODE] = 'binary diff not shown' | |
388 | chunks = [] |
|
388 | chunks = [] | |
389 |
|
389 | |||
390 | if op == 'removed' and chunks: |
|
390 | if op == 'removed' and chunks: | |
391 | # a way of seeing deleted content could perhaps be nice - but |
|
391 | # a way of seeing deleted content could perhaps be nice - but | |
392 | # not with the current UI |
|
392 | # not with the current UI | |
393 | chunks = [] |
|
393 | chunks = [] | |
394 |
|
394 | |||
395 | chunks.insert(0, [{ |
|
395 | chunks.insert(0, [{ | |
396 | 'old_lineno': '', |
|
396 | 'old_lineno': '', | |
397 | 'new_lineno': '', |
|
397 | 'new_lineno': '', | |
398 | 'action': 'context', |
|
398 | 'action': 'context', | |
399 | 'line': msg, |
|
399 | 'line': msg, | |
400 | } for _op, msg in stats['ops'].items() |
|
400 | } for _op, msg in stats['ops'].items() | |
401 | if _op not in [MOD_FILENODE]]) |
|
401 | if _op not in [MOD_FILENODE]]) | |
402 |
|
402 | |||
403 | _files.append({ |
|
403 | _files.append({ | |
404 | 'old_filename': head['a_path'], |
|
404 | 'old_filename': head['a_path'], | |
405 | 'filename': head['b_path'], |
|
405 | 'filename': head['b_path'], | |
406 | 'old_revision': head['a_blob_id'], |
|
406 | 'old_revision': head['a_blob_id'], | |
407 | 'new_revision': head['b_blob_id'], |
|
407 | 'new_revision': head['b_blob_id'], | |
408 | 'chunks': chunks, |
|
408 | 'chunks': chunks, | |
409 | 'operation': op, |
|
409 | 'operation': op, | |
410 | 'stats': stats, |
|
410 | 'stats': stats, | |
411 | }) |
|
411 | }) | |
412 |
|
412 | |||
413 | if not inline_diff: |
|
413 | if not inline_diff: | |
414 | return _files |
|
414 | return _files | |
415 |
|
415 | |||
416 | # highlight inline changes when one del is followed by one add |
|
416 | # highlight inline changes when one del is followed by one add | |
417 | for diff_data in _files: |
|
417 | for diff_data in _files: | |
418 | for chunk in diff_data['chunks']: |
|
418 | for chunk in diff_data['chunks']: | |
419 | lineiter = iter(chunk) |
|
419 | lineiter = iter(chunk) | |
420 | try: |
|
420 | try: | |
421 | peekline = next(lineiter) |
|
421 | peekline = next(lineiter) | |
422 | while True: |
|
422 | while True: | |
423 | # find a first del line |
|
423 | # find a first del line | |
424 | while peekline['action'] != 'del': |
|
424 | while peekline['action'] != 'del': | |
425 | peekline = next(lineiter) |
|
425 | peekline = next(lineiter) | |
426 | delline = peekline |
|
426 | delline = peekline | |
427 | peekline = next(lineiter) |
|
427 | peekline = next(lineiter) | |
428 | # if not followed by add, eat all following del lines |
|
428 | # if not followed by add, eat all following del lines | |
429 | if peekline['action'] != 'add': |
|
429 | if peekline['action'] != 'add': | |
430 | while peekline['action'] == 'del': |
|
430 | while peekline['action'] == 'del': | |
431 | peekline = next(lineiter) |
|
431 | peekline = next(lineiter) | |
432 | continue |
|
432 | continue | |
433 | # found an add - make sure it is the only one |
|
433 | # found an add - make sure it is the only one | |
434 | addline = peekline |
|
434 | addline = peekline | |
435 | try: |
|
435 | try: | |
436 | peekline = next(lineiter) |
|
436 | peekline = next(lineiter) | |
437 | except StopIteration: |
|
437 | except StopIteration: | |
438 | # add was last line - ok |
|
438 | # add was last line - ok | |
439 | _highlight_inline_diff(delline, addline) |
|
439 | _highlight_inline_diff(delline, addline) | |
440 | raise |
|
440 | raise | |
441 | if peekline['action'] != 'add': |
|
441 | if peekline['action'] != 'add': | |
442 | # there was only one add line - ok |
|
442 | # there was only one add line - ok | |
443 | _highlight_inline_diff(delline, addline) |
|
443 | _highlight_inline_diff(delline, addline) | |
444 | except StopIteration: |
|
444 | except StopIteration: | |
445 | pass |
|
445 | pass | |
446 |
|
446 | |||
447 | return _files |
|
447 | return _files | |
448 |
|
448 | |||
449 | def stat(self): |
|
449 | def stat(self): | |
450 | """ |
|
450 | """ | |
451 | Returns tuple of added, and removed lines for this instance |
|
451 | Returns tuple of added, and removed lines for this instance | |
452 | """ |
|
452 | """ | |
453 | return self.adds, self.removes |
|
453 | return self.adds, self.removes | |
454 |
|
454 | |||
455 |
|
455 | |||
456 | _escape_re = re.compile(r'(&)|(<)|(>)|(\t)|(\r)|(?<=.)( \n| $)') |
|
456 | _escape_re = re.compile(r'(&)|(<)|(>)|(\t)|(\r)|(?<=.)( \n| $)') | |
457 |
|
457 | |||
458 |
|
458 | |||
459 | def _escaper(string): |
|
459 | def _escaper(string): | |
460 | """ |
|
460 | """ | |
461 | Do HTML escaping/markup |
|
461 | Do HTML escaping/markup | |
462 | """ |
|
462 | """ | |
463 |
|
463 | |||
464 | def substitute(m): |
|
464 | def substitute(m): | |
465 | groups = m.groups() |
|
465 | groups = m.groups() | |
466 | if groups[0]: |
|
466 | if groups[0]: | |
467 | return '&' |
|
467 | return '&' | |
468 | if groups[1]: |
|
468 | if groups[1]: | |
469 | return '<' |
|
469 | return '<' | |
470 | if groups[2]: |
|
470 | if groups[2]: | |
471 | return '>' |
|
471 | return '>' | |
472 | if groups[3]: |
|
472 | if groups[3]: | |
473 | return '<u>\t</u>' |
|
473 | return '<u>\t</u>' | |
474 | if groups[4]: |
|
474 | if groups[4]: | |
475 | return '<u class="cr"></u>' |
|
475 | return '<u class="cr"></u>' | |
476 | if groups[5]: |
|
476 | if groups[5]: | |
477 | return ' <i></i>' |
|
477 | return ' <i></i>' | |
478 | assert False |
|
478 | assert False | |
479 |
|
479 | |||
480 | return _escape_re.sub(substitute, safe_str(string)) |
|
480 | return _escape_re.sub(substitute, safe_str(string)) | |
481 |
|
481 | |||
482 |
|
482 | |||
483 | _git_header_re = re.compile(br""" |
|
483 | _git_header_re = re.compile(br""" | |
484 | ^diff[ ]--git[ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n |
|
484 | ^diff[ ]--git[ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n | |
485 | (?:^old[ ]mode[ ](?P<old_mode>\d+)\n |
|
485 | (?:^old[ ]mode[ ](?P<old_mode>\d+)\n | |
486 | ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))? |
|
486 | ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))? | |
487 | (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n |
|
487 | (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n | |
488 | ^rename[ ]from[ ](?P<rename_from>.+)\n |
|
488 | ^rename[ ]from[ ](?P<rename_from>.+)\n | |
489 | ^rename[ ]to[ ](?P<rename_to>.+)(?:\n|$))? |
|
489 | ^rename[ ]to[ ](?P<rename_to>.+)(?:\n|$))? | |
490 | (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))? |
|
490 | (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))? | |
491 | (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))? |
|
491 | (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))? | |
492 | (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+) |
|
492 | (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+) | |
493 | \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))? |
|
493 | \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))? | |
494 | (?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))? |
|
494 | (?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))? | |
495 | (?:^---[ ](a/(?P<a_file>.+?)|/dev/null)\t?(?:\n|$))? |
|
495 | (?:^---[ ](a/(?P<a_file>.+?)|/dev/null)\t?(?:\n|$))? | |
496 | (?:^\+\+\+[ ](b/(?P<b_file>.+?)|/dev/null)\t?(?:\n|$))? |
|
496 | (?:^\+\+\+[ ](b/(?P<b_file>.+?)|/dev/null)\t?(?:\n|$))? | |
497 | """, re.VERBOSE | re.MULTILINE) |
|
497 | """, re.VERBOSE | re.MULTILINE) | |
498 |
|
498 | |||
499 |
|
499 | |||
500 | _hg_header_re = re.compile(br""" |
|
500 | _hg_header_re = re.compile(br""" | |
501 | ^diff[ ]--git[ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n |
|
501 | ^diff[ ]--git[ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n | |
502 | (?:^old[ ]mode[ ](?P<old_mode>\d+)\n |
|
502 | (?:^old[ ]mode[ ](?P<old_mode>\d+)\n | |
503 | ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))? |
|
503 | ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))? | |
504 | (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))? |
|
504 | (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))? | |
505 | (?:^rename[ ]from[ ](?P<rename_from>.+)\n |
|
505 | (?:^rename[ ]from[ ](?P<rename_from>.+)\n | |
506 | ^rename[ ]to[ ](?P<rename_to>.+)(?:\n|$))? |
|
506 | ^rename[ ]to[ ](?P<rename_to>.+)(?:\n|$))? | |
507 | (?:^copy[ ]from[ ](?P<copy_from>.+)\n |
|
507 | (?:^copy[ ]from[ ](?P<copy_from>.+)\n | |
508 | ^copy[ ]to[ ](?P<copy_to>.+)(?:\n|$))? |
|
508 | ^copy[ ]to[ ](?P<copy_to>.+)(?:\n|$))? | |
509 | (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))? |
|
509 | (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))? | |
510 | (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))? |
|
510 | (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))? | |
511 | (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+) |
|
511 | (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+) | |
512 | \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))? |
|
512 | \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))? | |
513 | (?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))? |
|
513 | (?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))? | |
514 | (?:^---[ ](a/(?P<a_file>.+?)|/dev/null)\t?(?:\n|$))? |
|
514 | (?:^---[ ](a/(?P<a_file>.+?)|/dev/null)\t?(?:\n|$))? | |
515 | (?:^\+\+\+[ ](b/(?P<b_file>.+?)|/dev/null)\t?(?:\n|$))? |
|
515 | (?:^\+\+\+[ ](b/(?P<b_file>.+?)|/dev/null)\t?(?:\n|$))? | |
516 | """, re.VERBOSE | re.MULTILINE) |
|
516 | """, re.VERBOSE | re.MULTILINE) | |
517 |
|
517 | |||
518 |
|
518 | |||
519 | _header_next_check = re.compile(br'''(?!@)(?!literal )(?!delta )''') |
|
519 | _header_next_check = re.compile(br'''(?!@)(?!literal )(?!delta )''') | |
520 |
|
520 | |||
521 |
|
521 | |||
522 | def _get_header(vcs, diff_chunk): |
|
522 | def _get_header(vcs, diff_chunk): | |
523 | """ |
|
523 | """ | |
524 | Parses a Git diff for a single file (header and chunks) and returns a tuple with: |
|
524 | Parses a Git diff for a single file (header and chunks) and returns a tuple with: | |
525 |
|
525 | |||
526 | 1. A dict with meta info: |
|
526 | 1. A dict with meta info: | |
527 |
|
527 | |||
528 | a_path, b_path, similarity_index, rename_from, rename_to, |
|
528 | a_path, b_path, similarity_index, rename_from, rename_to, | |
529 | old_mode, new_mode, new_file_mode, deleted_file_mode, |
|
529 | old_mode, new_mode, new_file_mode, deleted_file_mode, | |
530 | a_blob_id, b_blob_id, b_mode, a_file, b_file |
|
530 | a_blob_id, b_blob_id, b_mode, a_file, b_file | |
531 |
|
531 | |||
532 | 2. An iterator yielding lines with simple HTML markup. |
|
532 | 2. An iterator yielding lines with simple HTML markup. | |
533 | """ |
|
533 | """ | |
534 | match = None |
|
534 | match = None | |
535 | if vcs == 'git': |
|
535 | if vcs == 'git': | |
536 | match = _git_header_re.match(diff_chunk) |
|
536 | match = _git_header_re.match(diff_chunk) | |
537 | elif vcs == 'hg': |
|
537 | elif vcs == 'hg': | |
538 | match = _hg_header_re.match(diff_chunk) |
|
538 | match = _hg_header_re.match(diff_chunk) | |
539 | if match is None: |
|
539 | if match is None: | |
540 | raise Exception('diff not recognized as valid %s diff' % vcs) |
|
540 | raise Exception('diff not recognized as valid %s diff' % vcs) | |
541 | meta_info = match.groupdict() |
|
541 | meta_info = {k: None if v is None else safe_str(v) for k, v in match.groupdict().items()} | |
542 | rest = diff_chunk[match.end():] |
|
542 | rest = diff_chunk[match.end():] | |
543 | if rest and _header_next_check.match(rest): |
|
543 | if rest and _header_next_check.match(rest): | |
544 | raise Exception('cannot parse %s diff header: %r followed by %r' % (vcs, diff_chunk[:match.end()], rest[:1000])) |
|
544 | raise Exception('cannot parse %s diff header: %r followed by %r' % (vcs, safe_str(bytes(diff_chunk[:match.end()])), safe_str(bytes(rest[:1000])))) | |
545 | diff_lines = (_escaper(m.group(0)) for m in re.finditer(br'.*\n|.+$', rest)) # don't split on \r as str.splitlines do |
|
545 | diff_lines = (_escaper(m.group(0)) for m in re.finditer(br'.*\n|.+$', rest)) # don't split on \r as str.splitlines do | |
546 | return meta_info, diff_lines |
|
546 | return meta_info, diff_lines | |
547 |
|
547 | |||
548 |
|
548 | |||
549 | _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)') |
|
549 | _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)') | |
550 | _newline_marker = re.compile(r'^\\ No newline at end of file') |
|
550 | _newline_marker = re.compile(r'^\\ No newline at end of file') | |
551 |
|
551 | |||
552 |
|
552 | |||
553 | def _parse_lines(diff_lines): |
|
553 | def _parse_lines(diff_lines): | |
554 | """ |
|
554 | """ | |
555 | Given an iterator of diff body lines, parse them and return a dict per |
|
555 | Given an iterator of diff body lines, parse them and return a dict per | |
556 | line and added/removed totals. |
|
556 | line and added/removed totals. | |
557 | """ |
|
557 | """ | |
558 | added = deleted = 0 |
|
558 | added = deleted = 0 | |
559 | old_line = old_end = new_line = new_end = None |
|
559 | old_line = old_end = new_line = new_end = None | |
560 |
|
560 | |||
561 | chunks = [] |
|
561 | chunks = [] | |
562 | try: |
|
562 | try: | |
563 | line = next(diff_lines) |
|
563 | line = next(diff_lines) | |
564 |
|
564 | |||
565 | while True: |
|
565 | while True: | |
566 | lines = [] |
|
566 | lines = [] | |
567 | chunks.append(lines) |
|
567 | chunks.append(lines) | |
568 |
|
568 | |||
569 | match = _chunk_re.match(line) |
|
569 | match = _chunk_re.match(line) | |
570 |
|
570 | |||
571 | if not match: |
|
571 | if not match: | |
572 | raise Exception('error parsing diff @@ line %r' % line) |
|
572 | raise Exception('error parsing diff @@ line %r' % line) | |
573 |
|
573 | |||
574 | gr = match.groups() |
|
574 | gr = match.groups() | |
575 | (old_line, old_end, |
|
575 | (old_line, old_end, | |
576 | new_line, new_end) = [int(x or 1) for x in gr[:-1]] |
|
576 | new_line, new_end) = [int(x or 1) for x in gr[:-1]] | |
577 | old_line -= 1 |
|
577 | old_line -= 1 | |
578 | new_line -= 1 |
|
578 | new_line -= 1 | |
579 |
|
579 | |||
580 | context = len(gr) == 5 |
|
580 | context = len(gr) == 5 | |
581 | old_end += old_line |
|
581 | old_end += old_line | |
582 | new_end += new_line |
|
582 | new_end += new_line | |
583 |
|
583 | |||
584 | if context: |
|
584 | if context: | |
585 | # skip context only if it's first line |
|
585 | # skip context only if it's first line | |
586 | if int(gr[0]) > 1: |
|
586 | if int(gr[0]) > 1: | |
587 | lines.append({ |
|
587 | lines.append({ | |
588 | 'old_lineno': '...', |
|
588 | 'old_lineno': '...', | |
589 | 'new_lineno': '...', |
|
589 | 'new_lineno': '...', | |
590 | 'action': 'context', |
|
590 | 'action': 'context', | |
591 | 'line': line, |
|
591 | 'line': line, | |
592 | }) |
|
592 | }) | |
593 |
|
593 | |||
594 | line = next(diff_lines) |
|
594 | line = next(diff_lines) | |
595 |
|
595 | |||
596 | while old_line < old_end or new_line < new_end: |
|
596 | while old_line < old_end or new_line < new_end: | |
597 | if not line: |
|
597 | if not line: | |
598 | raise Exception('error parsing diff - empty line at -%s+%s' % (old_line, new_line)) |
|
598 | raise Exception('error parsing diff - empty line at -%s+%s' % (old_line, new_line)) | |
599 |
|
599 | |||
600 | affects_old = affects_new = False |
|
600 | affects_old = affects_new = False | |
601 |
|
601 | |||
602 | command = line[0] |
|
602 | command = line[0] | |
603 | if command == '+': |
|
603 | if command == '+': | |
604 | affects_new = True |
|
604 | affects_new = True | |
605 | action = 'add' |
|
605 | action = 'add' | |
606 | added += 1 |
|
606 | added += 1 | |
607 | elif command == '-': |
|
607 | elif command == '-': | |
608 | affects_old = True |
|
608 | affects_old = True | |
609 | action = 'del' |
|
609 | action = 'del' | |
610 | deleted += 1 |
|
610 | deleted += 1 | |
611 | elif command == ' ': |
|
611 | elif command == ' ': | |
612 | affects_old = affects_new = True |
|
612 | affects_old = affects_new = True | |
613 | action = 'unmod' |
|
613 | action = 'unmod' | |
614 | else: |
|
614 | else: | |
615 | raise Exception('error parsing diff - unknown command in line %r at -%s+%s' % (line, old_line, new_line)) |
|
615 | raise Exception('error parsing diff - unknown command in line %r at -%s+%s' % (line, old_line, new_line)) | |
616 |
|
616 | |||
617 | if not _newline_marker.match(line): |
|
617 | if not _newline_marker.match(line): | |
618 | old_line += affects_old |
|
618 | old_line += affects_old | |
619 | new_line += affects_new |
|
619 | new_line += affects_new | |
620 | lines.append({ |
|
620 | lines.append({ | |
621 | 'old_lineno': affects_old and old_line or '', |
|
621 | 'old_lineno': affects_old and old_line or '', | |
622 | 'new_lineno': affects_new and new_line or '', |
|
622 | 'new_lineno': affects_new and new_line or '', | |
623 | 'action': action, |
|
623 | 'action': action, | |
624 | 'line': line[1:], |
|
624 | 'line': line[1:], | |
625 | }) |
|
625 | }) | |
626 |
|
626 | |||
627 | line = next(diff_lines) |
|
627 | line = next(diff_lines) | |
628 |
|
628 | |||
629 | if _newline_marker.match(line): |
|
629 | if _newline_marker.match(line): | |
630 | # we need to append to lines, since this is not |
|
630 | # we need to append to lines, since this is not | |
631 | # counted in the line specs of diff |
|
631 | # counted in the line specs of diff | |
632 | lines.append({ |
|
632 | lines.append({ | |
633 | 'old_lineno': '...', |
|
633 | 'old_lineno': '...', | |
634 | 'new_lineno': '...', |
|
634 | 'new_lineno': '...', | |
635 | 'action': 'context', |
|
635 | 'action': 'context', | |
636 | 'line': line, |
|
636 | 'line': line, | |
637 | }) |
|
637 | }) | |
638 | line = next(diff_lines) |
|
638 | line = next(diff_lines) | |
639 | if old_line > old_end: |
|
639 | if old_line > old_end: | |
640 | raise Exception('error parsing diff - more than %s "-" lines at -%s+%s' % (old_end, old_line, new_line)) |
|
640 | raise Exception('error parsing diff - more than %s "-" lines at -%s+%s' % (old_end, old_line, new_line)) | |
641 | if new_line > new_end: |
|
641 | if new_line > new_end: | |
642 | raise Exception('error parsing diff - more than %s "+" lines at -%s+%s' % (new_end, old_line, new_line)) |
|
642 | raise Exception('error parsing diff - more than %s "+" lines at -%s+%s' % (new_end, old_line, new_line)) | |
643 | except StopIteration: |
|
643 | except StopIteration: | |
644 | pass |
|
644 | pass | |
645 | if old_line != old_end or new_line != new_end: |
|
645 | if old_line != old_end or new_line != new_end: | |
646 | raise Exception('diff processing broken when old %s<>%s or new %s<>%s line %r' % (old_line, old_end, new_line, new_end, line)) |
|
646 | raise Exception('diff processing broken when old %s<>%s or new %s<>%s line %r' % (old_line, old_end, new_line, new_end, line)) | |
647 |
|
647 | |||
648 | return chunks, added, deleted |
|
648 | return chunks, added, deleted | |
649 |
|
649 | |||
650 | # Used for inline highlighter word split, must match the substitutions in _escaper |
|
650 | # Used for inline highlighter word split, must match the substitutions in _escaper | |
651 | _token_re = re.compile(r'()(&|<|>|<u>\t</u>|<u class="cr"></u>| <i></i>|\W+?)') |
|
651 | _token_re = re.compile(r'()(&|<|>|<u>\t</u>|<u class="cr"></u>| <i></i>|\W+?)') | |
652 |
|
652 | |||
653 |
|
653 | |||
654 | def _highlight_inline_diff(old, new): |
|
654 | def _highlight_inline_diff(old, new): | |
655 | """ |
|
655 | """ | |
656 | Highlight simple add/remove in two lines given as info dicts. They are |
|
656 | Highlight simple add/remove in two lines given as info dicts. They are | |
657 | modified in place and given markup with <del>/<ins>. |
|
657 | modified in place and given markup with <del>/<ins>. | |
658 | """ |
|
658 | """ | |
659 | assert old['action'] == 'del' |
|
659 | assert old['action'] == 'del' | |
660 | assert new['action'] == 'add' |
|
660 | assert new['action'] == 'add' | |
661 |
|
661 | |||
662 | oldwords = _token_re.split(old['line']) |
|
662 | oldwords = _token_re.split(old['line']) | |
663 | newwords = _token_re.split(new['line']) |
|
663 | newwords = _token_re.split(new['line']) | |
664 | sequence = difflib.SequenceMatcher(None, oldwords, newwords) |
|
664 | sequence = difflib.SequenceMatcher(None, oldwords, newwords) | |
665 |
|
665 | |||
666 | oldfragments, newfragments = [], [] |
|
666 | oldfragments, newfragments = [], [] | |
667 | for tag, i1, i2, j1, j2 in sequence.get_opcodes(): |
|
667 | for tag, i1, i2, j1, j2 in sequence.get_opcodes(): | |
668 | oldfrag = ''.join(oldwords[i1:i2]) |
|
668 | oldfrag = ''.join(oldwords[i1:i2]) | |
669 | newfrag = ''.join(newwords[j1:j2]) |
|
669 | newfrag = ''.join(newwords[j1:j2]) | |
670 | if tag != 'equal': |
|
670 | if tag != 'equal': | |
671 | if oldfrag: |
|
671 | if oldfrag: | |
672 | oldfrag = '<del>%s</del>' % oldfrag |
|
672 | oldfrag = '<del>%s</del>' % oldfrag | |
673 | if newfrag: |
|
673 | if newfrag: | |
674 | newfrag = '<ins>%s</ins>' % newfrag |
|
674 | newfrag = '<ins>%s</ins>' % newfrag | |
675 | oldfragments.append(oldfrag) |
|
675 | oldfragments.append(oldfrag) | |
676 | newfragments.append(newfrag) |
|
676 | newfragments.append(newfrag) | |
677 |
|
677 | |||
678 | old['line'] = "".join(oldfragments) |
|
678 | old['line'] = "".join(oldfragments) | |
679 | new['line'] = "".join(newfragments) |
|
679 | new['line'] = "".join(newfragments) |
@@ -1,400 +1,400 b'' | |||||
1 | # -*- coding: utf-8 -*- |
|
1 | # -*- coding: utf-8 -*- | |
2 | # 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 | |
3 | # 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 | |
4 | # the Free Software Foundation, either version 3 of the License, or |
|
4 | # the Free Software Foundation, either version 3 of the License, or | |
5 | # (at your option) any later version. |
|
5 | # (at your option) any later version. | |
6 | # |
|
6 | # | |
7 | # This program is distributed in the hope that it will be useful, |
|
7 | # This program is distributed in the hope that it will be useful, | |
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
10 | # GNU General Public License for more details. |
|
10 | # GNU General Public License for more details. | |
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 | """ | |
15 | kallithea.lib.hooks |
|
15 | kallithea.lib.hooks | |
16 | ~~~~~~~~~~~~~~~~~~~ |
|
16 | ~~~~~~~~~~~~~~~~~~~ | |
17 |
|
17 | |||
18 | Hooks run by Kallithea |
|
18 | Hooks run by Kallithea | |
19 |
|
19 | |||
20 | This file was forked by the Kallithea project in July 2014. |
|
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: |
|
21 | Original author and date, and relevant copyright and licensing information is below: | |
22 | :created_on: Aug 6, 2010 |
|
22 | :created_on: Aug 6, 2010 | |
23 | :author: marcink |
|
23 | :author: marcink | |
24 | :copyright: (c) 2013 RhodeCode GmbH, and others. |
|
24 | :copyright: (c) 2013 RhodeCode GmbH, and others. | |
25 | :license: GPLv3, see LICENSE.md for more details. |
|
25 | :license: GPLv3, see LICENSE.md for more details. | |
26 | """ |
|
26 | """ | |
27 |
|
27 | |||
28 | import os |
|
28 | import os | |
29 | import sys |
|
29 | import sys | |
30 | import time |
|
30 | import time | |
31 |
|
31 | |||
32 | import mercurial.scmutil |
|
32 | import mercurial.scmutil | |
33 |
|
33 | |||
34 | from kallithea.lib import helpers as h |
|
34 | from kallithea.lib import helpers as h | |
35 | from kallithea.lib.exceptions import UserCreationError |
|
35 | from kallithea.lib.exceptions import UserCreationError | |
36 | from kallithea.lib.utils import action_logger, make_ui |
|
36 | from kallithea.lib.utils import action_logger, make_ui | |
37 | from kallithea.lib.utils2 import HookEnvironmentError, ascii_str, get_hook_environment, safe_bytes |
|
37 | from kallithea.lib.utils2 import HookEnvironmentError, ascii_str, get_hook_environment, safe_bytes, safe_str | |
38 | from kallithea.lib.vcs.backends.base import EmptyChangeset |
|
38 | from kallithea.lib.vcs.backends.base import EmptyChangeset | |
39 | from kallithea.model.db import Repository, User |
|
39 | from kallithea.model.db import Repository, User | |
40 |
|
40 | |||
41 |
|
41 | |||
42 | def _get_scm_size(alias, root_path): |
|
42 | def _get_scm_size(alias, root_path): | |
43 | if not alias.startswith('.'): |
|
43 | if not alias.startswith('.'): | |
44 | alias += '.' |
|
44 | alias += '.' | |
45 |
|
45 | |||
46 | size_scm, size_root = 0, 0 |
|
46 | size_scm, size_root = 0, 0 | |
47 | for path, dirs, files in os.walk(root_path): |
|
47 | for path, dirs, files in os.walk(root_path): | |
48 | if path.find(alias) != -1: |
|
48 | if path.find(alias) != -1: | |
49 | for f in files: |
|
49 | for f in files: | |
50 | try: |
|
50 | try: | |
51 | size_scm += os.path.getsize(os.path.join(path, f)) |
|
51 | size_scm += os.path.getsize(os.path.join(path, f)) | |
52 | except OSError: |
|
52 | except OSError: | |
53 | pass |
|
53 | pass | |
54 | else: |
|
54 | else: | |
55 | for f in files: |
|
55 | for f in files: | |
56 | try: |
|
56 | try: | |
57 | size_root += os.path.getsize(os.path.join(path, f)) |
|
57 | size_root += os.path.getsize(os.path.join(path, f)) | |
58 | except OSError: |
|
58 | except OSError: | |
59 | pass |
|
59 | pass | |
60 |
|
60 | |||
61 | size_scm_f = h.format_byte_size(size_scm) |
|
61 | size_scm_f = h.format_byte_size(size_scm) | |
62 | size_root_f = h.format_byte_size(size_root) |
|
62 | size_root_f = h.format_byte_size(size_root) | |
63 | size_total_f = h.format_byte_size(size_root + size_scm) |
|
63 | size_total_f = h.format_byte_size(size_root + size_scm) | |
64 |
|
64 | |||
65 | return size_scm_f, size_root_f, size_total_f |
|
65 | return size_scm_f, size_root_f, size_total_f | |
66 |
|
66 | |||
67 |
|
67 | |||
68 | def repo_size(ui, repo, hooktype=None, **kwargs): |
|
68 | def repo_size(ui, repo, hooktype=None, **kwargs): | |
69 | """Show size of Mercurial repository, to be called after push.""" |
|
69 | """Show size of Mercurial repository, to be called after push.""" | |
70 | size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', repo.root) |
|
70 | size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', safe_str(repo.root)) | |
71 |
|
71 | |||
72 | last_cs = repo[len(repo) - 1] |
|
72 | last_cs = repo[len(repo) - 1] | |
73 |
|
73 | |||
74 | msg = ('Repository size .hg: %s Checkout: %s Total: %s\n' |
|
74 | msg = ('Repository size .hg: %s Checkout: %s Total: %s\n' | |
75 | 'Last revision is now r%s:%s\n') % ( |
|
75 | 'Last revision is now r%s:%s\n') % ( | |
76 | size_hg_f, size_root_f, size_total_f, last_cs.rev(), ascii_str(last_cs.hex())[:12] |
|
76 | size_hg_f, size_root_f, size_total_f, last_cs.rev(), ascii_str(last_cs.hex())[:12] | |
77 | ) |
|
77 | ) | |
78 | ui.status(safe_bytes(msg)) |
|
78 | ui.status(safe_bytes(msg)) | |
79 |
|
79 | |||
80 |
|
80 | |||
81 | def log_pull_action(ui, repo, **kwargs): |
|
81 | def log_pull_action(ui, repo, **kwargs): | |
82 | """Logs user last pull action |
|
82 | """Logs user last pull action | |
83 |
|
83 | |||
84 | Called as Mercurial hook outgoing.pull_logger or from Kallithea before invoking Git. |
|
84 | Called as Mercurial hook outgoing.pull_logger or from Kallithea before invoking Git. | |
85 |
|
85 | |||
86 | Does *not* use the action from the hook environment but is always 'pull'. |
|
86 | Does *not* use the action from the hook environment but is always 'pull'. | |
87 | """ |
|
87 | """ | |
88 | ex = get_hook_environment() |
|
88 | ex = get_hook_environment() | |
89 |
|
89 | |||
90 | user = User.get_by_username(ex.username) |
|
90 | user = User.get_by_username(ex.username) | |
91 | action = 'pull' |
|
91 | action = 'pull' | |
92 | action_logger(user, action, ex.repository, ex.ip, commit=True) |
|
92 | action_logger(user, action, ex.repository, ex.ip, commit=True) | |
93 | # extension hook call |
|
93 | # extension hook call | |
94 | from kallithea import EXTENSIONS |
|
94 | from kallithea import EXTENSIONS | |
95 | callback = getattr(EXTENSIONS, 'PULL_HOOK', None) |
|
95 | callback = getattr(EXTENSIONS, 'PULL_HOOK', None) | |
96 | if callable(callback): |
|
96 | if callable(callback): | |
97 | kw = {} |
|
97 | kw = {} | |
98 | kw.update(ex) |
|
98 | kw.update(ex) | |
99 | callback(**kw) |
|
99 | callback(**kw) | |
100 |
|
100 | |||
101 | return 0 |
|
101 | return 0 | |
102 |
|
102 | |||
103 |
|
103 | |||
104 | def log_push_action(ui, repo, node, node_last, **kwargs): |
|
104 | def log_push_action(ui, repo, node, node_last, **kwargs): | |
105 | """ |
|
105 | """ | |
106 | Entry point for Mercurial hook changegroup.push_logger. |
|
106 | Entry point for Mercurial hook changegroup.push_logger. | |
107 |
|
107 | |||
108 | The pushed changesets is given by the revset 'node:node_last'. |
|
108 | The pushed changesets is given by the revset 'node:node_last'. | |
109 |
|
109 | |||
110 | Note: This hook is not only logging, but also the side effect invalidating |
|
110 | Note: This hook is not only logging, but also the side effect invalidating | |
111 | cahes! The function should perhaps be renamed. |
|
111 | cahes! The function should perhaps be renamed. | |
112 | """ |
|
112 | """ | |
113 | revs = [ascii_str(repo[r].hex()) for r in mercurial.scmutil.revrange(repo, [b'%s:%s' % (node, node_last)])] |
|
113 | revs = [ascii_str(repo[r].hex()) for r in mercurial.scmutil.revrange(repo, [b'%s:%s' % (node, node_last)])] | |
114 | process_pushed_raw_ids(revs) |
|
114 | process_pushed_raw_ids(revs) | |
115 | return 0 |
|
115 | return 0 | |
116 |
|
116 | |||
117 |
|
117 | |||
118 | def process_pushed_raw_ids(revs): |
|
118 | def process_pushed_raw_ids(revs): | |
119 | """ |
|
119 | """ | |
120 | Register that changes have been added to the repo - log the action *and* invalidate caches. |
|
120 | Register that changes have been added to the repo - log the action *and* invalidate caches. | |
121 |
|
121 | |||
122 | Called from Mercurial changegroup.push_logger calling hook log_push_action, |
|
122 | Called from Mercurial changegroup.push_logger calling hook log_push_action, | |
123 | or from the Git post-receive hook calling handle_git_post_receive ... |
|
123 | or from the Git post-receive hook calling handle_git_post_receive ... | |
124 | or from scm _handle_push. |
|
124 | or from scm _handle_push. | |
125 | """ |
|
125 | """ | |
126 | ex = get_hook_environment() |
|
126 | ex = get_hook_environment() | |
127 |
|
127 | |||
128 | action = '%s:%s' % (ex.action, ','.join(revs)) |
|
128 | action = '%s:%s' % (ex.action, ','.join(revs)) | |
129 | action_logger(ex.username, action, ex.repository, ex.ip, commit=True) |
|
129 | action_logger(ex.username, action, ex.repository, ex.ip, commit=True) | |
130 |
|
130 | |||
131 | from kallithea.model.scm import ScmModel |
|
131 | from kallithea.model.scm import ScmModel | |
132 | ScmModel().mark_for_invalidation(ex.repository) |
|
132 | ScmModel().mark_for_invalidation(ex.repository) | |
133 |
|
133 | |||
134 | # extension hook call |
|
134 | # extension hook call | |
135 | from kallithea import EXTENSIONS |
|
135 | from kallithea import EXTENSIONS | |
136 | callback = getattr(EXTENSIONS, 'PUSH_HOOK', None) |
|
136 | callback = getattr(EXTENSIONS, 'PUSH_HOOK', None) | |
137 | if callable(callback): |
|
137 | if callable(callback): | |
138 | kw = {'pushed_revs': revs} |
|
138 | kw = {'pushed_revs': revs} | |
139 | kw.update(ex) |
|
139 | kw.update(ex) | |
140 | callback(**kw) |
|
140 | callback(**kw) | |
141 |
|
141 | |||
142 |
|
142 | |||
143 | def log_create_repository(repository_dict, created_by, **kwargs): |
|
143 | def log_create_repository(repository_dict, created_by, **kwargs): | |
144 | """ |
|
144 | """ | |
145 | Post create repository Hook. |
|
145 | Post create repository Hook. | |
146 |
|
146 | |||
147 | :param repository: dict dump of repository object |
|
147 | :param repository: dict dump of repository object | |
148 | :param created_by: username who created repository |
|
148 | :param created_by: username who created repository | |
149 |
|
149 | |||
150 | available keys of repository_dict: |
|
150 | available keys of repository_dict: | |
151 |
|
151 | |||
152 | 'repo_type', |
|
152 | 'repo_type', | |
153 | 'description', |
|
153 | 'description', | |
154 | 'private', |
|
154 | 'private', | |
155 | 'created_on', |
|
155 | 'created_on', | |
156 | 'enable_downloads', |
|
156 | 'enable_downloads', | |
157 | 'repo_id', |
|
157 | 'repo_id', | |
158 | 'owner_id', |
|
158 | 'owner_id', | |
159 | 'enable_statistics', |
|
159 | 'enable_statistics', | |
160 | 'clone_uri', |
|
160 | 'clone_uri', | |
161 | 'fork_id', |
|
161 | 'fork_id', | |
162 | 'group_id', |
|
162 | 'group_id', | |
163 | 'repo_name' |
|
163 | 'repo_name' | |
164 |
|
164 | |||
165 | """ |
|
165 | """ | |
166 | from kallithea import EXTENSIONS |
|
166 | from kallithea import EXTENSIONS | |
167 | callback = getattr(EXTENSIONS, 'CREATE_REPO_HOOK', None) |
|
167 | callback = getattr(EXTENSIONS, 'CREATE_REPO_HOOK', None) | |
168 | if callable(callback): |
|
168 | if callable(callback): | |
169 | kw = {} |
|
169 | kw = {} | |
170 | kw.update(repository_dict) |
|
170 | kw.update(repository_dict) | |
171 | kw.update({'created_by': created_by}) |
|
171 | kw.update({'created_by': created_by}) | |
172 | kw.update(kwargs) |
|
172 | kw.update(kwargs) | |
173 | return callback(**kw) |
|
173 | return callback(**kw) | |
174 |
|
174 | |||
175 | return 0 |
|
175 | return 0 | |
176 |
|
176 | |||
177 |
|
177 | |||
178 | def check_allowed_create_user(user_dict, created_by, **kwargs): |
|
178 | def check_allowed_create_user(user_dict, created_by, **kwargs): | |
179 | # pre create hooks |
|
179 | # pre create hooks | |
180 | from kallithea import EXTENSIONS |
|
180 | from kallithea import EXTENSIONS | |
181 | callback = getattr(EXTENSIONS, 'PRE_CREATE_USER_HOOK', None) |
|
181 | callback = getattr(EXTENSIONS, 'PRE_CREATE_USER_HOOK', None) | |
182 | if callable(callback): |
|
182 | if callable(callback): | |
183 | allowed, reason = callback(created_by=created_by, **user_dict) |
|
183 | allowed, reason = callback(created_by=created_by, **user_dict) | |
184 | if not allowed: |
|
184 | if not allowed: | |
185 | raise UserCreationError(reason) |
|
185 | raise UserCreationError(reason) | |
186 |
|
186 | |||
187 |
|
187 | |||
188 | def log_create_user(user_dict, created_by, **kwargs): |
|
188 | def log_create_user(user_dict, created_by, **kwargs): | |
189 | """ |
|
189 | """ | |
190 | Post create user Hook. |
|
190 | Post create user Hook. | |
191 |
|
191 | |||
192 | :param user_dict: dict dump of user object |
|
192 | :param user_dict: dict dump of user object | |
193 |
|
193 | |||
194 | available keys for user_dict: |
|
194 | available keys for user_dict: | |
195 |
|
195 | |||
196 | 'username', |
|
196 | 'username', | |
197 | 'full_name_or_username', |
|
197 | 'full_name_or_username', | |
198 | 'full_contact', |
|
198 | 'full_contact', | |
199 | 'user_id', |
|
199 | 'user_id', | |
200 | 'name', |
|
200 | 'name', | |
201 | 'firstname', |
|
201 | 'firstname', | |
202 | 'short_contact', |
|
202 | 'short_contact', | |
203 | 'admin', |
|
203 | 'admin', | |
204 | 'lastname', |
|
204 | 'lastname', | |
205 | 'ip_addresses', |
|
205 | 'ip_addresses', | |
206 | 'ldap_dn', |
|
206 | 'ldap_dn', | |
207 | 'email', |
|
207 | 'email', | |
208 | 'api_key', |
|
208 | 'api_key', | |
209 | 'last_login', |
|
209 | 'last_login', | |
210 | 'full_name', |
|
210 | 'full_name', | |
211 | 'active', |
|
211 | 'active', | |
212 | 'password', |
|
212 | 'password', | |
213 | 'emails', |
|
213 | 'emails', | |
214 |
|
214 | |||
215 | """ |
|
215 | """ | |
216 | from kallithea import EXTENSIONS |
|
216 | from kallithea import EXTENSIONS | |
217 | callback = getattr(EXTENSIONS, 'CREATE_USER_HOOK', None) |
|
217 | callback = getattr(EXTENSIONS, 'CREATE_USER_HOOK', None) | |
218 | if callable(callback): |
|
218 | if callable(callback): | |
219 | return callback(created_by=created_by, **user_dict) |
|
219 | return callback(created_by=created_by, **user_dict) | |
220 |
|
220 | |||
221 | return 0 |
|
221 | return 0 | |
222 |
|
222 | |||
223 |
|
223 | |||
224 | def log_delete_repository(repository_dict, deleted_by, **kwargs): |
|
224 | def log_delete_repository(repository_dict, deleted_by, **kwargs): | |
225 | """ |
|
225 | """ | |
226 | Post delete repository Hook. |
|
226 | Post delete repository Hook. | |
227 |
|
227 | |||
228 | :param repository: dict dump of repository object |
|
228 | :param repository: dict dump of repository object | |
229 | :param deleted_by: username who deleted the repository |
|
229 | :param deleted_by: username who deleted the repository | |
230 |
|
230 | |||
231 | available keys of repository_dict: |
|
231 | available keys of repository_dict: | |
232 |
|
232 | |||
233 | 'repo_type', |
|
233 | 'repo_type', | |
234 | 'description', |
|
234 | 'description', | |
235 | 'private', |
|
235 | 'private', | |
236 | 'created_on', |
|
236 | 'created_on', | |
237 | 'enable_downloads', |
|
237 | 'enable_downloads', | |
238 | 'repo_id', |
|
238 | 'repo_id', | |
239 | 'owner_id', |
|
239 | 'owner_id', | |
240 | 'enable_statistics', |
|
240 | 'enable_statistics', | |
241 | 'clone_uri', |
|
241 | 'clone_uri', | |
242 | 'fork_id', |
|
242 | 'fork_id', | |
243 | 'group_id', |
|
243 | 'group_id', | |
244 | 'repo_name' |
|
244 | 'repo_name' | |
245 |
|
245 | |||
246 | """ |
|
246 | """ | |
247 | from kallithea import EXTENSIONS |
|
247 | from kallithea import EXTENSIONS | |
248 | callback = getattr(EXTENSIONS, 'DELETE_REPO_HOOK', None) |
|
248 | callback = getattr(EXTENSIONS, 'DELETE_REPO_HOOK', None) | |
249 | if callable(callback): |
|
249 | if callable(callback): | |
250 | kw = {} |
|
250 | kw = {} | |
251 | kw.update(repository_dict) |
|
251 | kw.update(repository_dict) | |
252 | kw.update({'deleted_by': deleted_by, |
|
252 | kw.update({'deleted_by': deleted_by, | |
253 | 'deleted_on': time.time()}) |
|
253 | 'deleted_on': time.time()}) | |
254 | kw.update(kwargs) |
|
254 | kw.update(kwargs) | |
255 | return callback(**kw) |
|
255 | return callback(**kw) | |
256 |
|
256 | |||
257 | return 0 |
|
257 | return 0 | |
258 |
|
258 | |||
259 |
|
259 | |||
260 | def log_delete_user(user_dict, deleted_by, **kwargs): |
|
260 | def log_delete_user(user_dict, deleted_by, **kwargs): | |
261 | """ |
|
261 | """ | |
262 | Post delete user Hook. |
|
262 | Post delete user Hook. | |
263 |
|
263 | |||
264 | :param user_dict: dict dump of user object |
|
264 | :param user_dict: dict dump of user object | |
265 |
|
265 | |||
266 | available keys for user_dict: |
|
266 | available keys for user_dict: | |
267 |
|
267 | |||
268 | 'username', |
|
268 | 'username', | |
269 | 'full_name_or_username', |
|
269 | 'full_name_or_username', | |
270 | 'full_contact', |
|
270 | 'full_contact', | |
271 | 'user_id', |
|
271 | 'user_id', | |
272 | 'name', |
|
272 | 'name', | |
273 | 'firstname', |
|
273 | 'firstname', | |
274 | 'short_contact', |
|
274 | 'short_contact', | |
275 | 'admin', |
|
275 | 'admin', | |
276 | 'lastname', |
|
276 | 'lastname', | |
277 | 'ip_addresses', |
|
277 | 'ip_addresses', | |
278 | 'ldap_dn', |
|
278 | 'ldap_dn', | |
279 | 'email', |
|
279 | 'email', | |
280 | 'api_key', |
|
280 | 'api_key', | |
281 | 'last_login', |
|
281 | 'last_login', | |
282 | 'full_name', |
|
282 | 'full_name', | |
283 | 'active', |
|
283 | 'active', | |
284 | 'password', |
|
284 | 'password', | |
285 | 'emails', |
|
285 | 'emails', | |
286 |
|
286 | |||
287 | """ |
|
287 | """ | |
288 | from kallithea import EXTENSIONS |
|
288 | from kallithea import EXTENSIONS | |
289 | callback = getattr(EXTENSIONS, 'DELETE_USER_HOOK', None) |
|
289 | callback = getattr(EXTENSIONS, 'DELETE_USER_HOOK', None) | |
290 | if callable(callback): |
|
290 | if callable(callback): | |
291 | return callback(deleted_by=deleted_by, **user_dict) |
|
291 | return callback(deleted_by=deleted_by, **user_dict) | |
292 |
|
292 | |||
293 | return 0 |
|
293 | return 0 | |
294 |
|
294 | |||
295 |
|
295 | |||
296 | def _hook_environment(repo_path): |
|
296 | def _hook_environment(repo_path): | |
297 | """ |
|
297 | """ | |
298 | Create a light-weight environment for stand-alone scripts and return an UI and the |
|
298 | Create a light-weight environment for stand-alone scripts and return an UI and the | |
299 | db repository. |
|
299 | db repository. | |
300 |
|
300 | |||
301 | Git hooks are executed as subprocess of Git while Kallithea is waiting, and |
|
301 | Git hooks are executed as subprocess of Git while Kallithea is waiting, and | |
302 | they thus need enough info to be able to create an app environment and |
|
302 | they thus need enough info to be able to create an app environment and | |
303 | connect to the database. |
|
303 | connect to the database. | |
304 | """ |
|
304 | """ | |
305 | import paste.deploy |
|
305 | import paste.deploy | |
306 | import kallithea.config.middleware |
|
306 | import kallithea.config.middleware | |
307 |
|
307 | |||
308 | extras = get_hook_environment() |
|
308 | extras = get_hook_environment() | |
309 |
|
309 | |||
310 | path_to_ini_file = extras['config'] |
|
310 | path_to_ini_file = extras['config'] | |
311 | kallithea.CONFIG = paste.deploy.appconfig('config:' + path_to_ini_file) |
|
311 | kallithea.CONFIG = paste.deploy.appconfig('config:' + path_to_ini_file) | |
312 | #logging.config.fileConfig(ini_file_path) # Note: we are in a different process - don't use configured logging |
|
312 | #logging.config.fileConfig(ini_file_path) # Note: we are in a different process - don't use configured logging | |
313 | kallithea.config.middleware.make_app(kallithea.CONFIG.global_conf, **kallithea.CONFIG.local_conf) |
|
313 | kallithea.config.middleware.make_app(kallithea.CONFIG.global_conf, **kallithea.CONFIG.local_conf) | |
314 |
|
314 | |||
315 | # fix if it's not a bare repo |
|
315 | # fix if it's not a bare repo | |
316 | if repo_path.endswith(os.sep + '.git'): |
|
316 | if repo_path.endswith(os.sep + '.git'): | |
317 | repo_path = repo_path[:-5] |
|
317 | repo_path = repo_path[:-5] | |
318 |
|
318 | |||
319 | repo = Repository.get_by_full_path(repo_path) |
|
319 | repo = Repository.get_by_full_path(repo_path) | |
320 | if not repo: |
|
320 | if not repo: | |
321 | raise OSError('Repository %s not found in database' % repo_path) |
|
321 | raise OSError('Repository %s not found in database' % repo_path) | |
322 |
|
322 | |||
323 | baseui = make_ui() |
|
323 | baseui = make_ui() | |
324 | return baseui, repo |
|
324 | return baseui, repo | |
325 |
|
325 | |||
326 |
|
326 | |||
327 | def handle_git_pre_receive(repo_path, git_stdin_lines): |
|
327 | def handle_git_pre_receive(repo_path, git_stdin_lines): | |
328 | """Called from Git pre-receive hook""" |
|
328 | """Called from Git pre-receive hook""" | |
329 | # Currently unused. TODO: remove? |
|
329 | # Currently unused. TODO: remove? | |
330 | return 0 |
|
330 | return 0 | |
331 |
|
331 | |||
332 |
|
332 | |||
333 | def handle_git_post_receive(repo_path, git_stdin_lines): |
|
333 | def handle_git_post_receive(repo_path, git_stdin_lines): | |
334 | """Called from Git post-receive hook""" |
|
334 | """Called from Git post-receive hook""" | |
335 | try: |
|
335 | try: | |
336 | baseui, repo = _hook_environment(repo_path) |
|
336 | baseui, repo = _hook_environment(repo_path) | |
337 | except HookEnvironmentError as e: |
|
337 | except HookEnvironmentError as e: | |
338 | sys.stderr.write("Skipping Kallithea Git post-recieve hook %r.\nGit was apparently not invoked by Kallithea: %s\n" % (sys.argv[0], e)) |
|
338 | sys.stderr.write("Skipping Kallithea Git post-recieve hook %r.\nGit was apparently not invoked by Kallithea: %s\n" % (sys.argv[0], e)) | |
339 | return 0 |
|
339 | return 0 | |
340 |
|
340 | |||
341 | # the post push hook should never use the cached instance |
|
341 | # the post push hook should never use the cached instance | |
342 | scm_repo = repo.scm_instance_no_cache() |
|
342 | scm_repo = repo.scm_instance_no_cache() | |
343 |
|
343 | |||
344 | rev_data = [] |
|
344 | rev_data = [] | |
345 | for l in git_stdin_lines: |
|
345 | for l in git_stdin_lines: | |
346 | old_rev, new_rev, ref = l.strip().split(' ') |
|
346 | old_rev, new_rev, ref = l.strip().split(' ') | |
347 | _ref_data = ref.split('/') |
|
347 | _ref_data = ref.split('/') | |
348 | if _ref_data[1] in ['tags', 'heads']: |
|
348 | if _ref_data[1] in ['tags', 'heads']: | |
349 | rev_data.append({'old_rev': old_rev, |
|
349 | rev_data.append({'old_rev': old_rev, | |
350 | 'new_rev': new_rev, |
|
350 | 'new_rev': new_rev, | |
351 | 'ref': ref, |
|
351 | 'ref': ref, | |
352 | 'type': _ref_data[1], |
|
352 | 'type': _ref_data[1], | |
353 | 'name': '/'.join(_ref_data[2:])}) |
|
353 | 'name': '/'.join(_ref_data[2:])}) | |
354 |
|
354 | |||
355 | git_revs = [] |
|
355 | git_revs = [] | |
356 | for push_ref in rev_data: |
|
356 | for push_ref in rev_data: | |
357 | _type = push_ref['type'] |
|
357 | _type = push_ref['type'] | |
358 | if _type == 'heads': |
|
358 | if _type == 'heads': | |
359 | if push_ref['old_rev'] == EmptyChangeset().raw_id: |
|
359 | if push_ref['old_rev'] == EmptyChangeset().raw_id: | |
360 | # update the symbolic ref if we push new repo |
|
360 | # update the symbolic ref if we push new repo | |
361 | if scm_repo.is_empty(): |
|
361 | if scm_repo.is_empty(): | |
362 | scm_repo._repo.refs.set_symbolic_ref( |
|
362 | scm_repo._repo.refs.set_symbolic_ref( | |
363 | b'HEAD', |
|
363 | b'HEAD', | |
364 | b'refs/heads/%s' % safe_bytes(push_ref['name'])) |
|
364 | b'refs/heads/%s' % safe_bytes(push_ref['name'])) | |
365 |
|
365 | |||
366 | # build exclude list without the ref |
|
366 | # build exclude list without the ref | |
367 | cmd = ['for-each-ref', '--format=%(refname)', 'refs/heads/*'] |
|
367 | cmd = ['for-each-ref', '--format=%(refname)', 'refs/heads/*'] | |
368 | stdout = scm_repo.run_git_command(cmd) |
|
368 | stdout = scm_repo.run_git_command(cmd) | |
369 | ref = push_ref['ref'] |
|
369 | ref = push_ref['ref'] | |
370 | heads = [head for head in stdout.splitlines() if head != ref] |
|
370 | heads = [head for head in stdout.splitlines() if head != ref] | |
371 | # now list the git revs while excluding from the list |
|
371 | # now list the git revs while excluding from the list | |
372 | cmd = ['log', push_ref['new_rev'], '--reverse', '--pretty=format:%H'] |
|
372 | cmd = ['log', push_ref['new_rev'], '--reverse', '--pretty=format:%H'] | |
373 | cmd.append('--not') |
|
373 | cmd.append('--not') | |
374 | cmd.extend(heads) # empty list is ok |
|
374 | cmd.extend(heads) # empty list is ok | |
375 | stdout = scm_repo.run_git_command(cmd) |
|
375 | stdout = scm_repo.run_git_command(cmd) | |
376 | git_revs += stdout.splitlines() |
|
376 | git_revs += stdout.splitlines() | |
377 |
|
377 | |||
378 | elif push_ref['new_rev'] == EmptyChangeset().raw_id: |
|
378 | elif push_ref['new_rev'] == EmptyChangeset().raw_id: | |
379 | # delete branch case |
|
379 | # delete branch case | |
380 | git_revs += ['delete_branch=>%s' % push_ref['name']] |
|
380 | git_revs += ['delete_branch=>%s' % push_ref['name']] | |
381 | else: |
|
381 | else: | |
382 | cmd = ['log', '%(old_rev)s..%(new_rev)s' % push_ref, |
|
382 | cmd = ['log', '%(old_rev)s..%(new_rev)s' % push_ref, | |
383 | '--reverse', '--pretty=format:%H'] |
|
383 | '--reverse', '--pretty=format:%H'] | |
384 | stdout = scm_repo.run_git_command(cmd) |
|
384 | stdout = scm_repo.run_git_command(cmd) | |
385 | git_revs += stdout.splitlines() |
|
385 | git_revs += stdout.splitlines() | |
386 |
|
386 | |||
387 | elif _type == 'tags': |
|
387 | elif _type == 'tags': | |
388 | git_revs += ['tag=>%s' % push_ref['name']] |
|
388 | git_revs += ['tag=>%s' % push_ref['name']] | |
389 |
|
389 | |||
390 | process_pushed_raw_ids(git_revs) |
|
390 | process_pushed_raw_ids(git_revs) | |
391 |
|
391 | |||
392 | return 0 |
|
392 | return 0 | |
393 |
|
393 | |||
394 |
|
394 | |||
395 | # Almost exactly like Mercurial contrib/hg-ssh: |
|
395 | # Almost exactly like Mercurial contrib/hg-ssh: | |
396 | def rejectpush(ui, **kwargs): |
|
396 | def rejectpush(ui, **kwargs): | |
397 | """Mercurial hook to be installed as pretxnopen and prepushkey for read-only repos""" |
|
397 | """Mercurial hook to be installed as pretxnopen and prepushkey for read-only repos""" | |
398 | ex = get_hook_environment() |
|
398 | ex = get_hook_environment() | |
399 | ui.warn(safe_bytes("Push access to %r denied\n" % ex.repository)) |
|
399 | ui.warn(safe_bytes("Push access to %r denied\n" % ex.repository)) | |
400 | return 1 |
|
400 | return 1 |
@@ -1,646 +1,646 b'' | |||||
1 | # -*- coding: utf-8 -*- |
|
1 | # -*- coding: utf-8 -*- | |
2 | # 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 | |
3 | # 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 | |
4 | # the Free Software Foundation, either version 3 of the License, or |
|
4 | # the Free Software Foundation, either version 3 of the License, or | |
5 | # (at your option) any later version. |
|
5 | # (at your option) any later version. | |
6 | # |
|
6 | # | |
7 | # This program is distributed in the hope that it will be useful, |
|
7 | # This program is distributed in the hope that it will be useful, | |
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
10 | # GNU General Public License for more details. |
|
10 | # GNU General Public License for more details. | |
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 | """ | |
15 | kallithea.lib.utils |
|
15 | kallithea.lib.utils | |
16 | ~~~~~~~~~~~~~~~~~~~ |
|
16 | ~~~~~~~~~~~~~~~~~~~ | |
17 |
|
17 | |||
18 | Utilities library for Kallithea |
|
18 | Utilities library for Kallithea | |
19 |
|
19 | |||
20 | This file was forked by the Kallithea project in July 2014. |
|
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: |
|
21 | Original author and date, and relevant copyright and licensing information is below: | |
22 | :created_on: Apr 18, 2010 |
|
22 | :created_on: Apr 18, 2010 | |
23 | :author: marcink |
|
23 | :author: marcink | |
24 | :copyright: (c) 2013 RhodeCode GmbH, and others. |
|
24 | :copyright: (c) 2013 RhodeCode GmbH, and others. | |
25 | :license: GPLv3, see LICENSE.md for more details. |
|
25 | :license: GPLv3, see LICENSE.md for more details. | |
26 | """ |
|
26 | """ | |
27 |
|
27 | |||
28 | import datetime |
|
28 | import datetime | |
29 | import logging |
|
29 | import logging | |
30 | import os |
|
30 | import os | |
31 | import re |
|
31 | import re | |
32 | import sys |
|
32 | import sys | |
33 | import traceback |
|
33 | import traceback | |
34 | from distutils.version import StrictVersion |
|
34 | from distutils.version import StrictVersion | |
35 |
|
35 | |||
36 | import beaker.cache |
|
36 | import beaker.cache | |
37 | import mercurial.config |
|
37 | import mercurial.config | |
38 | import mercurial.ui |
|
38 | import mercurial.ui | |
39 | from tg.i18n import ugettext as _ |
|
39 | from tg.i18n import ugettext as _ | |
40 |
|
40 | |||
41 | import kallithea.config.conf |
|
41 | import kallithea.config.conf | |
42 | from kallithea.lib.exceptions import HgsubversionImportError |
|
42 | from kallithea.lib.exceptions import HgsubversionImportError | |
43 | from kallithea.lib.utils2 import ascii_bytes, aslist, get_current_authuser, safe_bytes |
|
43 | from kallithea.lib.utils2 import ascii_bytes, aslist, get_current_authuser, safe_bytes, safe_str | |
44 | from kallithea.lib.vcs.backends.git.repository import GitRepository |
|
44 | from kallithea.lib.vcs.backends.git.repository import GitRepository | |
45 | from kallithea.lib.vcs.backends.hg.repository import MercurialRepository |
|
45 | from kallithea.lib.vcs.backends.hg.repository import MercurialRepository | |
46 | from kallithea.lib.vcs.conf import settings |
|
46 | from kallithea.lib.vcs.conf import settings | |
47 | from kallithea.lib.vcs.exceptions import RepositoryError, VCSError |
|
47 | from kallithea.lib.vcs.exceptions import RepositoryError, VCSError | |
48 | from kallithea.lib.vcs.utils.fakemod import create_module |
|
48 | from kallithea.lib.vcs.utils.fakemod import create_module | |
49 | from kallithea.lib.vcs.utils.helpers import get_scm |
|
49 | from kallithea.lib.vcs.utils.helpers import get_scm | |
50 | from kallithea.model import meta |
|
50 | from kallithea.model import meta | |
51 | from kallithea.model.db import RepoGroup, Repository, Setting, Ui, User, UserGroup, UserLog |
|
51 | from kallithea.model.db import RepoGroup, Repository, Setting, Ui, User, UserGroup, UserLog | |
52 |
|
52 | |||
53 |
|
53 | |||
54 | log = logging.getLogger(__name__) |
|
54 | log = logging.getLogger(__name__) | |
55 |
|
55 | |||
56 | REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}_.*') |
|
56 | REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}_.*') | |
57 |
|
57 | |||
58 |
|
58 | |||
59 | #============================================================================== |
|
59 | #============================================================================== | |
60 | # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS |
|
60 | # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS | |
61 | #============================================================================== |
|
61 | #============================================================================== | |
62 | def get_repo_slug(request): |
|
62 | def get_repo_slug(request): | |
63 | _repo = request.environ['pylons.routes_dict'].get('repo_name') |
|
63 | _repo = request.environ['pylons.routes_dict'].get('repo_name') | |
64 | if _repo: |
|
64 | if _repo: | |
65 | _repo = _repo.rstrip('/') |
|
65 | _repo = _repo.rstrip('/') | |
66 | return _repo |
|
66 | return _repo | |
67 |
|
67 | |||
68 |
|
68 | |||
69 | def get_repo_group_slug(request): |
|
69 | def get_repo_group_slug(request): | |
70 | _group = request.environ['pylons.routes_dict'].get('group_name') |
|
70 | _group = request.environ['pylons.routes_dict'].get('group_name') | |
71 | if _group: |
|
71 | if _group: | |
72 | _group = _group.rstrip('/') |
|
72 | _group = _group.rstrip('/') | |
73 | return _group |
|
73 | return _group | |
74 |
|
74 | |||
75 |
|
75 | |||
76 | def get_user_group_slug(request): |
|
76 | def get_user_group_slug(request): | |
77 | _group = request.environ['pylons.routes_dict'].get('id') |
|
77 | _group = request.environ['pylons.routes_dict'].get('id') | |
78 | _group = UserGroup.get(_group) |
|
78 | _group = UserGroup.get(_group) | |
79 | if _group: |
|
79 | if _group: | |
80 | return _group.users_group_name |
|
80 | return _group.users_group_name | |
81 | return None |
|
81 | return None | |
82 |
|
82 | |||
83 |
|
83 | |||
84 | def _get_permanent_id(s): |
|
84 | def _get_permanent_id(s): | |
85 | """Helper for decoding stable URLs with repo ID. For a string like '_123' |
|
85 | """Helper for decoding stable URLs with repo ID. For a string like '_123' | |
86 | return 123. |
|
86 | return 123. | |
87 | """ |
|
87 | """ | |
88 | by_id_match = re.match(r'^_(\d+)$', s) |
|
88 | by_id_match = re.match(r'^_(\d+)$', s) | |
89 | if by_id_match is None: |
|
89 | if by_id_match is None: | |
90 | return None |
|
90 | return None | |
91 | return int(by_id_match.group(1)) |
|
91 | return int(by_id_match.group(1)) | |
92 |
|
92 | |||
93 |
|
93 | |||
94 | def fix_repo_id_name(path): |
|
94 | def fix_repo_id_name(path): | |
95 | """ |
|
95 | """ | |
96 | Rewrite repo_name for _<ID> permanent URLs. |
|
96 | Rewrite repo_name for _<ID> permanent URLs. | |
97 |
|
97 | |||
98 | Given a path, if the first path element is like _<ID>, return the path with |
|
98 | Given a path, if the first path element is like _<ID>, return the path with | |
99 | this part expanded to the corresponding full repo name, else return the |
|
99 | this part expanded to the corresponding full repo name, else return the | |
100 | provided path. |
|
100 | provided path. | |
101 | """ |
|
101 | """ | |
102 | first, rest = path, '' |
|
102 | first, rest = path, '' | |
103 | if '/' in path: |
|
103 | if '/' in path: | |
104 | first, rest_ = path.split('/', 1) |
|
104 | first, rest_ = path.split('/', 1) | |
105 | rest = '/' + rest_ |
|
105 | rest = '/' + rest_ | |
106 | repo_id = _get_permanent_id(first) |
|
106 | repo_id = _get_permanent_id(first) | |
107 | if repo_id is not None: |
|
107 | if repo_id is not None: | |
108 | repo = Repository.get(repo_id) |
|
108 | repo = Repository.get(repo_id) | |
109 | if repo is not None: |
|
109 | if repo is not None: | |
110 | return repo.repo_name + rest |
|
110 | return repo.repo_name + rest | |
111 | return path |
|
111 | return path | |
112 |
|
112 | |||
113 |
|
113 | |||
114 | def action_logger(user, action, repo, ipaddr='', commit=False): |
|
114 | def action_logger(user, action, repo, ipaddr='', commit=False): | |
115 | """ |
|
115 | """ | |
116 | Action logger for various actions made by users |
|
116 | Action logger for various actions made by users | |
117 |
|
117 | |||
118 | :param user: user that made this action, can be a unique username string or |
|
118 | :param user: user that made this action, can be a unique username string or | |
119 | object containing user_id attribute |
|
119 | object containing user_id attribute | |
120 | :param action: action to log, should be on of predefined unique actions for |
|
120 | :param action: action to log, should be on of predefined unique actions for | |
121 | easy translations |
|
121 | easy translations | |
122 | :param repo: string name of repository or object containing repo_id, |
|
122 | :param repo: string name of repository or object containing repo_id, | |
123 | that action was made on |
|
123 | that action was made on | |
124 | :param ipaddr: optional IP address from what the action was made |
|
124 | :param ipaddr: optional IP address from what the action was made | |
125 |
|
125 | |||
126 | """ |
|
126 | """ | |
127 |
|
127 | |||
128 | # if we don't get explicit IP address try to get one from registered user |
|
128 | # if we don't get explicit IP address try to get one from registered user | |
129 | # in tmpl context var |
|
129 | # in tmpl context var | |
130 | if not ipaddr: |
|
130 | if not ipaddr: | |
131 | ipaddr = getattr(get_current_authuser(), 'ip_addr', '') |
|
131 | ipaddr = getattr(get_current_authuser(), 'ip_addr', '') | |
132 |
|
132 | |||
133 | if getattr(user, 'user_id', None): |
|
133 | if getattr(user, 'user_id', None): | |
134 | user_obj = User.get(user.user_id) |
|
134 | user_obj = User.get(user.user_id) | |
135 | elif isinstance(user, str): |
|
135 | elif isinstance(user, str): | |
136 | user_obj = User.get_by_username(user) |
|
136 | user_obj = User.get_by_username(user) | |
137 | else: |
|
137 | else: | |
138 | raise Exception('You have to provide a user object or a username') |
|
138 | raise Exception('You have to provide a user object or a username') | |
139 |
|
139 | |||
140 | if getattr(repo, 'repo_id', None): |
|
140 | if getattr(repo, 'repo_id', None): | |
141 | repo_obj = Repository.get(repo.repo_id) |
|
141 | repo_obj = Repository.get(repo.repo_id) | |
142 | repo_name = repo_obj.repo_name |
|
142 | repo_name = repo_obj.repo_name | |
143 | elif isinstance(repo, str): |
|
143 | elif isinstance(repo, str): | |
144 | repo_name = repo.lstrip('/') |
|
144 | repo_name = repo.lstrip('/') | |
145 | repo_obj = Repository.get_by_repo_name(repo_name) |
|
145 | repo_obj = Repository.get_by_repo_name(repo_name) | |
146 | else: |
|
146 | else: | |
147 | repo_obj = None |
|
147 | repo_obj = None | |
148 | repo_name = u'' |
|
148 | repo_name = u'' | |
149 |
|
149 | |||
150 | user_log = UserLog() |
|
150 | user_log = UserLog() | |
151 | user_log.user_id = user_obj.user_id |
|
151 | user_log.user_id = user_obj.user_id | |
152 | user_log.username = user_obj.username |
|
152 | user_log.username = user_obj.username | |
153 | user_log.action = action |
|
153 | user_log.action = action | |
154 |
|
154 | |||
155 | user_log.repository = repo_obj |
|
155 | user_log.repository = repo_obj | |
156 | user_log.repository_name = repo_name |
|
156 | user_log.repository_name = repo_name | |
157 |
|
157 | |||
158 | user_log.action_date = datetime.datetime.now() |
|
158 | user_log.action_date = datetime.datetime.now() | |
159 | user_log.user_ip = ipaddr |
|
159 | user_log.user_ip = ipaddr | |
160 | meta.Session().add(user_log) |
|
160 | meta.Session().add(user_log) | |
161 |
|
161 | |||
162 | log.info('Logging action:%s on %s by user:%s ip:%s', |
|
162 | log.info('Logging action:%s on %s by user:%s ip:%s', | |
163 | action, repo, user_obj, ipaddr) |
|
163 | action, repo, user_obj, ipaddr) | |
164 | if commit: |
|
164 | if commit: | |
165 | meta.Session().commit() |
|
165 | meta.Session().commit() | |
166 |
|
166 | |||
167 |
|
167 | |||
168 | def get_filesystem_repos(path): |
|
168 | def get_filesystem_repos(path): | |
169 | """ |
|
169 | """ | |
170 | Scans given path for repos and return (name,(type,path)) tuple |
|
170 | Scans given path for repos and return (name,(type,path)) tuple | |
171 |
|
171 | |||
172 | :param path: path to scan for repositories |
|
172 | :param path: path to scan for repositories | |
173 | :param recursive: recursive search and return names with subdirs in front |
|
173 | :param recursive: recursive search and return names with subdirs in front | |
174 | """ |
|
174 | """ | |
175 |
|
175 | |||
176 | # remove ending slash for better results |
|
176 | # remove ending slash for better results | |
177 | path = path.rstrip(os.sep) |
|
177 | path = path.rstrip(os.sep) | |
178 | log.debug('now scanning in %s', path) |
|
178 | log.debug('now scanning in %s', path) | |
179 |
|
179 | |||
180 | def isdir(*n): |
|
180 | def isdir(*n): | |
181 | return os.path.isdir(os.path.join(*n)) |
|
181 | return os.path.isdir(os.path.join(*n)) | |
182 |
|
182 | |||
183 | for root, dirs, _files in os.walk(path): |
|
183 | for root, dirs, _files in os.walk(path): | |
184 | recurse_dirs = [] |
|
184 | recurse_dirs = [] | |
185 | for subdir in dirs: |
|
185 | for subdir in dirs: | |
186 | # skip removed repos |
|
186 | # skip removed repos | |
187 | if REMOVED_REPO_PAT.match(subdir): |
|
187 | if REMOVED_REPO_PAT.match(subdir): | |
188 | continue |
|
188 | continue | |
189 |
|
189 | |||
190 | # skip .<something> dirs TODO: rly? then we should prevent creating them ... |
|
190 | # skip .<something> dirs TODO: rly? then we should prevent creating them ... | |
191 | if subdir.startswith('.'): |
|
191 | if subdir.startswith('.'): | |
192 | continue |
|
192 | continue | |
193 |
|
193 | |||
194 | cur_path = os.path.join(root, subdir) |
|
194 | cur_path = os.path.join(root, subdir) | |
195 | if isdir(cur_path, '.git'): |
|
195 | if isdir(cur_path, '.git'): | |
196 | log.warning('ignoring non-bare Git repo: %s', cur_path) |
|
196 | log.warning('ignoring non-bare Git repo: %s', cur_path) | |
197 | continue |
|
197 | continue | |
198 |
|
198 | |||
199 | if (isdir(cur_path, '.hg') or |
|
199 | if (isdir(cur_path, '.hg') or | |
200 | isdir(cur_path, '.svn') or |
|
200 | isdir(cur_path, '.svn') or | |
201 | isdir(cur_path, 'objects') and (isdir(cur_path, 'refs') or |
|
201 | isdir(cur_path, 'objects') and (isdir(cur_path, 'refs') or | |
202 | os.path.isfile(os.path.join(cur_path, 'packed-refs')))): |
|
202 | os.path.isfile(os.path.join(cur_path, 'packed-refs')))): | |
203 |
|
203 | |||
204 | if not os.access(cur_path, os.R_OK) or not os.access(cur_path, os.X_OK): |
|
204 | if not os.access(cur_path, os.R_OK) or not os.access(cur_path, os.X_OK): | |
205 | log.warning('ignoring repo path without access: %s', cur_path) |
|
205 | log.warning('ignoring repo path without access: %s', cur_path) | |
206 | continue |
|
206 | continue | |
207 |
|
207 | |||
208 | if not os.access(cur_path, os.W_OK): |
|
208 | if not os.access(cur_path, os.W_OK): | |
209 | log.warning('repo path without write access: %s', cur_path) |
|
209 | log.warning('repo path without write access: %s', cur_path) | |
210 |
|
210 | |||
211 | try: |
|
211 | try: | |
212 | scm_info = get_scm(cur_path) |
|
212 | scm_info = get_scm(cur_path) | |
213 | assert cur_path.startswith(path) |
|
213 | assert cur_path.startswith(path) | |
214 | repo_path = cur_path[len(path) + 1:] |
|
214 | repo_path = cur_path[len(path) + 1:] | |
215 | yield repo_path, scm_info |
|
215 | yield repo_path, scm_info | |
216 | continue # no recursion |
|
216 | continue # no recursion | |
217 | except VCSError: |
|
217 | except VCSError: | |
218 | # We should perhaps ignore such broken repos, but especially |
|
218 | # We should perhaps ignore such broken repos, but especially | |
219 | # the bare git detection is unreliable so we dive into it |
|
219 | # the bare git detection is unreliable so we dive into it | |
220 | pass |
|
220 | pass | |
221 |
|
221 | |||
222 | recurse_dirs.append(subdir) |
|
222 | recurse_dirs.append(subdir) | |
223 |
|
223 | |||
224 | dirs[:] = recurse_dirs |
|
224 | dirs[:] = recurse_dirs | |
225 |
|
225 | |||
226 |
|
226 | |||
227 | def is_valid_repo_uri(repo_type, url, ui): |
|
227 | def is_valid_repo_uri(repo_type, url, ui): | |
228 | """Check if the url seems like a valid remote repo location - raise an Exception if any problems""" |
|
228 | """Check if the url seems like a valid remote repo location - raise an Exception if any problems""" | |
229 | if repo_type == 'hg': |
|
229 | if repo_type == 'hg': | |
230 | if url.startswith('http') or url.startswith('ssh'): |
|
230 | if url.startswith('http') or url.startswith('ssh'): | |
231 | # initially check if it's at least the proper URL |
|
231 | # initially check if it's at least the proper URL | |
232 | # or does it pass basic auth |
|
232 | # or does it pass basic auth | |
233 | MercurialRepository._check_url(url, ui) |
|
233 | MercurialRepository._check_url(url, ui) | |
234 | elif url.startswith('svn+http'): |
|
234 | elif url.startswith('svn+http'): | |
235 | try: |
|
235 | try: | |
236 | from hgsubversion.svnrepo import svnremoterepo |
|
236 | from hgsubversion.svnrepo import svnremoterepo | |
237 | except ImportError: |
|
237 | except ImportError: | |
238 | raise HgsubversionImportError(_('Unable to activate hgsubversion support. ' |
|
238 | raise HgsubversionImportError(_('Unable to activate hgsubversion support. ' | |
239 | 'The "hgsubversion" library is missing')) |
|
239 | 'The "hgsubversion" library is missing')) | |
240 | svnremoterepo(ui, url).svn.uuid |
|
240 | svnremoterepo(ui, url).svn.uuid | |
241 | elif url.startswith('git+http'): |
|
241 | elif url.startswith('git+http'): | |
242 | raise NotImplementedError() |
|
242 | raise NotImplementedError() | |
243 | else: |
|
243 | else: | |
244 | raise Exception('URI %s not allowed' % (url,)) |
|
244 | raise Exception('URI %s not allowed' % (url,)) | |
245 |
|
245 | |||
246 | elif repo_type == 'git': |
|
246 | elif repo_type == 'git': | |
247 | if url.startswith('http') or url.startswith('git'): |
|
247 | if url.startswith('http') or url.startswith('git'): | |
248 | # initially check if it's at least the proper URL |
|
248 | # initially check if it's at least the proper URL | |
249 | # or does it pass basic auth |
|
249 | # or does it pass basic auth | |
250 | GitRepository._check_url(url) |
|
250 | GitRepository._check_url(url) | |
251 | elif url.startswith('svn+http'): |
|
251 | elif url.startswith('svn+http'): | |
252 | raise NotImplementedError() |
|
252 | raise NotImplementedError() | |
253 | elif url.startswith('hg+http'): |
|
253 | elif url.startswith('hg+http'): | |
254 | raise NotImplementedError() |
|
254 | raise NotImplementedError() | |
255 | else: |
|
255 | else: | |
256 | raise Exception('URI %s not allowed' % (url)) |
|
256 | raise Exception('URI %s not allowed' % (url)) | |
257 |
|
257 | |||
258 |
|
258 | |||
259 | def is_valid_repo(repo_name, base_path, scm=None): |
|
259 | def is_valid_repo(repo_name, base_path, scm=None): | |
260 | """ |
|
260 | """ | |
261 | Returns True if given path is a valid repository False otherwise. |
|
261 | Returns True if given path is a valid repository False otherwise. | |
262 | If scm param is given also compare if given scm is the same as expected |
|
262 | If scm param is given also compare if given scm is the same as expected | |
263 | from scm parameter |
|
263 | from scm parameter | |
264 |
|
264 | |||
265 | :param repo_name: |
|
265 | :param repo_name: | |
266 | :param base_path: |
|
266 | :param base_path: | |
267 | :param scm: |
|
267 | :param scm: | |
268 |
|
268 | |||
269 | :return True: if given path is a valid repository |
|
269 | :return True: if given path is a valid repository | |
270 | """ |
|
270 | """ | |
271 | # TODO: paranoid security checks? |
|
271 | # TODO: paranoid security checks? | |
272 | full_path = os.path.join(base_path, repo_name) |
|
272 | full_path = os.path.join(base_path, repo_name) | |
273 |
|
273 | |||
274 | try: |
|
274 | try: | |
275 | scm_ = get_scm(full_path) |
|
275 | scm_ = get_scm(full_path) | |
276 | if scm: |
|
276 | if scm: | |
277 | return scm_[0] == scm |
|
277 | return scm_[0] == scm | |
278 | return True |
|
278 | return True | |
279 | except VCSError: |
|
279 | except VCSError: | |
280 | return False |
|
280 | return False | |
281 |
|
281 | |||
282 |
|
282 | |||
283 | def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False): |
|
283 | def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False): | |
284 | """ |
|
284 | """ | |
285 | Returns True if given path is a repository group False otherwise |
|
285 | Returns True if given path is a repository group False otherwise | |
286 |
|
286 | |||
287 | :param repo_name: |
|
287 | :param repo_name: | |
288 | :param base_path: |
|
288 | :param base_path: | |
289 | """ |
|
289 | """ | |
290 | full_path = os.path.join(base_path, repo_group_name) |
|
290 | full_path = os.path.join(base_path, repo_group_name) | |
291 |
|
291 | |||
292 | # check if it's not a repo |
|
292 | # check if it's not a repo | |
293 | if is_valid_repo(repo_group_name, base_path): |
|
293 | if is_valid_repo(repo_group_name, base_path): | |
294 | return False |
|
294 | return False | |
295 |
|
295 | |||
296 | try: |
|
296 | try: | |
297 | # we need to check bare git repos at higher level |
|
297 | # we need to check bare git repos at higher level | |
298 | # since we might match branches/hooks/info/objects or possible |
|
298 | # since we might match branches/hooks/info/objects or possible | |
299 | # other things inside bare git repo |
|
299 | # other things inside bare git repo | |
300 | get_scm(os.path.dirname(full_path)) |
|
300 | get_scm(os.path.dirname(full_path)) | |
301 | return False |
|
301 | return False | |
302 | except VCSError: |
|
302 | except VCSError: | |
303 | pass |
|
303 | pass | |
304 |
|
304 | |||
305 | # check if it's a valid path |
|
305 | # check if it's a valid path | |
306 | if skip_path_check or os.path.isdir(full_path): |
|
306 | if skip_path_check or os.path.isdir(full_path): | |
307 | return True |
|
307 | return True | |
308 |
|
308 | |||
309 | return False |
|
309 | return False | |
310 |
|
310 | |||
311 |
|
311 | |||
312 | # propagated from mercurial documentation |
|
312 | # propagated from mercurial documentation | |
313 | ui_sections = ['alias', 'auth', |
|
313 | ui_sections = ['alias', 'auth', | |
314 | 'decode/encode', 'defaults', |
|
314 | 'decode/encode', 'defaults', | |
315 | 'diff', 'email', |
|
315 | 'diff', 'email', | |
316 | 'extensions', 'format', |
|
316 | 'extensions', 'format', | |
317 | 'merge-patterns', 'merge-tools', |
|
317 | 'merge-patterns', 'merge-tools', | |
318 | 'hooks', 'http_proxy', |
|
318 | 'hooks', 'http_proxy', | |
319 | 'smtp', 'patch', |
|
319 | 'smtp', 'patch', | |
320 | 'paths', 'profiling', |
|
320 | 'paths', 'profiling', | |
321 | 'server', 'trusted', |
|
321 | 'server', 'trusted', | |
322 | 'ui', 'web', ] |
|
322 | 'ui', 'web', ] | |
323 |
|
323 | |||
324 |
|
324 | |||
325 | def make_ui(repo_path=None): |
|
325 | def make_ui(repo_path=None): | |
326 | """ |
|
326 | """ | |
327 | Create an Mercurial 'ui' object based on database Ui settings, possibly |
|
327 | Create an Mercurial 'ui' object based on database Ui settings, possibly | |
328 | augmenting with content from a hgrc file. |
|
328 | augmenting with content from a hgrc file. | |
329 | """ |
|
329 | """ | |
330 | baseui = mercurial.ui.ui() |
|
330 | baseui = mercurial.ui.ui() | |
331 |
|
331 | |||
332 | # clean the baseui object |
|
332 | # clean the baseui object | |
333 | baseui._ocfg = mercurial.config.config() |
|
333 | baseui._ocfg = mercurial.config.config() | |
334 | baseui._ucfg = mercurial.config.config() |
|
334 | baseui._ucfg = mercurial.config.config() | |
335 | baseui._tcfg = mercurial.config.config() |
|
335 | baseui._tcfg = mercurial.config.config() | |
336 |
|
336 | |||
337 | sa = meta.Session() |
|
337 | sa = meta.Session() | |
338 | for ui_ in sa.query(Ui).all(): |
|
338 | for ui_ in sa.query(Ui).all(): | |
339 | if ui_.ui_active: |
|
339 | if ui_.ui_active: | |
340 | log.debug('config from db: [%s] %s=%r', ui_.ui_section, |
|
340 | log.debug('config from db: [%s] %s=%r', ui_.ui_section, | |
341 | ui_.ui_key, ui_.ui_value) |
|
341 | ui_.ui_key, ui_.ui_value) | |
342 | baseui.setconfig(ascii_bytes(ui_.ui_section), ascii_bytes(ui_.ui_key), |
|
342 | baseui.setconfig(ascii_bytes(ui_.ui_section), ascii_bytes(ui_.ui_key), | |
343 | b'' if ui_.ui_value is None else safe_bytes(ui_.ui_value)) |
|
343 | b'' if ui_.ui_value is None else safe_bytes(ui_.ui_value)) | |
344 |
|
344 | |||
345 | # force set push_ssl requirement to False, Kallithea handles that |
|
345 | # force set push_ssl requirement to False, Kallithea handles that | |
346 | baseui.setconfig(b'web', b'push_ssl', False) |
|
346 | baseui.setconfig(b'web', b'push_ssl', False) | |
347 | baseui.setconfig(b'web', b'allow_push', b'*') |
|
347 | baseui.setconfig(b'web', b'allow_push', b'*') | |
348 | # prevent interactive questions for ssh password / passphrase |
|
348 | # prevent interactive questions for ssh password / passphrase | |
349 | ssh = baseui.config(b'ui', b'ssh', default=b'ssh') |
|
349 | ssh = baseui.config(b'ui', b'ssh', default=b'ssh') | |
350 | baseui.setconfig(b'ui', b'ssh', b'%s -oBatchMode=yes -oIdentitiesOnly=yes' % ssh) |
|
350 | baseui.setconfig(b'ui', b'ssh', b'%s -oBatchMode=yes -oIdentitiesOnly=yes' % ssh) | |
351 | # push / pull hooks |
|
351 | # push / pull hooks | |
352 | baseui.setconfig(b'hooks', b'changegroup.kallithea_log_push_action', b'python:kallithea.lib.hooks.log_push_action') |
|
352 | baseui.setconfig(b'hooks', b'changegroup.kallithea_log_push_action', b'python:kallithea.lib.hooks.log_push_action') | |
353 | baseui.setconfig(b'hooks', b'outgoing.kallithea_log_pull_action', b'python:kallithea.lib.hooks.log_pull_action') |
|
353 | baseui.setconfig(b'hooks', b'outgoing.kallithea_log_pull_action', b'python:kallithea.lib.hooks.log_pull_action') | |
354 |
|
354 | |||
355 | if repo_path is not None: |
|
355 | if repo_path is not None: | |
356 | hgrc_path = os.path.join(repo_path, '.hg', 'hgrc') |
|
356 | hgrc_path = os.path.join(repo_path, '.hg', 'hgrc') | |
357 | if os.path.isfile(hgrc_path): |
|
357 | if os.path.isfile(hgrc_path): | |
358 | log.debug('reading hgrc from %s', hgrc_path) |
|
358 | log.debug('reading hgrc from %s', hgrc_path) | |
359 | cfg = mercurial.config.config() |
|
359 | cfg = mercurial.config.config() | |
360 | cfg.read(safe_bytes(hgrc_path)) |
|
360 | cfg.read(safe_bytes(hgrc_path)) | |
361 | for section in ui_sections: |
|
361 | for section in ui_sections: | |
362 | for k, v in cfg.items(section): |
|
362 | for k, v in cfg.items(section): | |
363 | log.debug('config from file: [%s] %s=%s', section, k, v) |
|
363 | log.debug('config from file: [%s] %s=%s', section, k, v) | |
364 | baseui.setconfig(ascii_bytes(section), ascii_bytes(k), safe_bytes(v)) |
|
364 | baseui.setconfig(ascii_bytes(section), ascii_bytes(k), safe_bytes(v)) | |
365 | else: |
|
365 | else: | |
366 | log.debug('hgrc file is not present at %s, skipping...', hgrc_path) |
|
366 | log.debug('hgrc file is not present at %s, skipping...', hgrc_path) | |
367 |
|
367 | |||
368 | return baseui |
|
368 | return baseui | |
369 |
|
369 | |||
370 |
|
370 | |||
371 | def set_app_settings(config): |
|
371 | def set_app_settings(config): | |
372 | """ |
|
372 | """ | |
373 | Updates app config with new settings from database |
|
373 | Updates app config with new settings from database | |
374 |
|
374 | |||
375 | :param config: |
|
375 | :param config: | |
376 | """ |
|
376 | """ | |
377 | hgsettings = Setting.get_app_settings() |
|
377 | hgsettings = Setting.get_app_settings() | |
378 | for k, v in hgsettings.items(): |
|
378 | for k, v in hgsettings.items(): | |
379 | config[k] = v |
|
379 | config[k] = v | |
380 |
|
380 | |||
381 |
|
381 | |||
382 | def set_vcs_config(config): |
|
382 | def set_vcs_config(config): | |
383 | """ |
|
383 | """ | |
384 | Patch VCS config with some Kallithea specific stuff |
|
384 | Patch VCS config with some Kallithea specific stuff | |
385 |
|
385 | |||
386 | :param config: kallithea.CONFIG |
|
386 | :param config: kallithea.CONFIG | |
387 | """ |
|
387 | """ | |
388 | settings.BACKENDS = { |
|
388 | settings.BACKENDS = { | |
389 | 'hg': 'kallithea.lib.vcs.backends.hg.MercurialRepository', |
|
389 | 'hg': 'kallithea.lib.vcs.backends.hg.MercurialRepository', | |
390 | 'git': 'kallithea.lib.vcs.backends.git.GitRepository', |
|
390 | 'git': 'kallithea.lib.vcs.backends.git.GitRepository', | |
391 | } |
|
391 | } | |
392 |
|
392 | |||
393 | settings.GIT_EXECUTABLE_PATH = config.get('git_path', 'git') |
|
393 | settings.GIT_EXECUTABLE_PATH = config.get('git_path', 'git') | |
394 | settings.GIT_REV_FILTER = config.get('git_rev_filter', '--all').strip() |
|
394 | settings.GIT_REV_FILTER = config.get('git_rev_filter', '--all').strip() | |
395 | settings.DEFAULT_ENCODINGS = aslist(config.get('default_encoding', |
|
395 | settings.DEFAULT_ENCODINGS = aslist(config.get('default_encoding', | |
396 | 'utf-8'), sep=',') |
|
396 | 'utf-8'), sep=',') | |
397 |
|
397 | |||
398 |
|
398 | |||
399 | def set_indexer_config(config): |
|
399 | def set_indexer_config(config): | |
400 | """ |
|
400 | """ | |
401 | Update Whoosh index mapping |
|
401 | Update Whoosh index mapping | |
402 |
|
402 | |||
403 | :param config: kallithea.CONFIG |
|
403 | :param config: kallithea.CONFIG | |
404 | """ |
|
404 | """ | |
405 | log.debug('adding extra into INDEX_EXTENSIONS') |
|
405 | log.debug('adding extra into INDEX_EXTENSIONS') | |
406 | kallithea.config.conf.INDEX_EXTENSIONS.extend(re.split(r'\s+', config.get('index.extensions', ''))) |
|
406 | kallithea.config.conf.INDEX_EXTENSIONS.extend(re.split(r'\s+', config.get('index.extensions', ''))) | |
407 |
|
407 | |||
408 | log.debug('adding extra into INDEX_FILENAMES') |
|
408 | log.debug('adding extra into INDEX_FILENAMES') | |
409 | kallithea.config.conf.INDEX_FILENAMES.extend(re.split(r'\s+', config.get('index.filenames', ''))) |
|
409 | kallithea.config.conf.INDEX_FILENAMES.extend(re.split(r'\s+', config.get('index.filenames', ''))) | |
410 |
|
410 | |||
411 |
|
411 | |||
412 | def map_groups(path): |
|
412 | def map_groups(path): | |
413 | """ |
|
413 | """ | |
414 | Given a full path to a repository, create all nested groups that this |
|
414 | Given a full path to a repository, create all nested groups that this | |
415 | repo is inside. This function creates parent-child relationships between |
|
415 | repo is inside. This function creates parent-child relationships between | |
416 | groups and creates default perms for all new groups. |
|
416 | groups and creates default perms for all new groups. | |
417 |
|
417 | |||
418 | :param paths: full path to repository |
|
418 | :param paths: full path to repository | |
419 | """ |
|
419 | """ | |
420 | from kallithea.model.repo_group import RepoGroupModel |
|
420 | from kallithea.model.repo_group import RepoGroupModel | |
421 | sa = meta.Session() |
|
421 | sa = meta.Session() | |
422 | groups = path.split(Repository.url_sep()) |
|
422 | groups = path.split(Repository.url_sep()) | |
423 | parent = None |
|
423 | parent = None | |
424 | group = None |
|
424 | group = None | |
425 |
|
425 | |||
426 | # last element is repo in nested groups structure |
|
426 | # last element is repo in nested groups structure | |
427 | groups = groups[:-1] |
|
427 | groups = groups[:-1] | |
428 | rgm = RepoGroupModel() |
|
428 | rgm = RepoGroupModel() | |
429 | owner = User.get_first_admin() |
|
429 | owner = User.get_first_admin() | |
430 | for lvl, group_name in enumerate(groups): |
|
430 | for lvl, group_name in enumerate(groups): | |
431 | group_name = u'/'.join(groups[:lvl] + [group_name]) |
|
431 | group_name = u'/'.join(groups[:lvl] + [group_name]) | |
432 | group = RepoGroup.get_by_group_name(group_name) |
|
432 | group = RepoGroup.get_by_group_name(group_name) | |
433 | desc = '%s group' % group_name |
|
433 | desc = '%s group' % group_name | |
434 |
|
434 | |||
435 | # skip folders that are now removed repos |
|
435 | # skip folders that are now removed repos | |
436 | if REMOVED_REPO_PAT.match(group_name): |
|
436 | if REMOVED_REPO_PAT.match(group_name): | |
437 | break |
|
437 | break | |
438 |
|
438 | |||
439 | if group is None: |
|
439 | if group is None: | |
440 | log.debug('creating group level: %s group_name: %s', |
|
440 | log.debug('creating group level: %s group_name: %s', | |
441 | lvl, group_name) |
|
441 | lvl, group_name) | |
442 | group = RepoGroup(group_name, parent) |
|
442 | group = RepoGroup(group_name, parent) | |
443 | group.group_description = desc |
|
443 | group.group_description = desc | |
444 | group.owner = owner |
|
444 | group.owner = owner | |
445 | sa.add(group) |
|
445 | sa.add(group) | |
446 | rgm._create_default_perms(group) |
|
446 | rgm._create_default_perms(group) | |
447 | sa.flush() |
|
447 | sa.flush() | |
448 |
|
448 | |||
449 | parent = group |
|
449 | parent = group | |
450 | return group |
|
450 | return group | |
451 |
|
451 | |||
452 |
|
452 | |||
453 | def repo2db_mapper(initial_repo_dict, remove_obsolete=False, |
|
453 | def repo2db_mapper(initial_repo_dict, remove_obsolete=False, | |
454 | install_git_hooks=False, user=None, overwrite_git_hooks=False): |
|
454 | install_git_hooks=False, user=None, overwrite_git_hooks=False): | |
455 | """ |
|
455 | """ | |
456 | maps all repos given in initial_repo_dict, non existing repositories |
|
456 | maps all repos given in initial_repo_dict, non existing repositories | |
457 | are created, if remove_obsolete is True it also check for db entries |
|
457 | are created, if remove_obsolete is True it also check for db entries | |
458 | that are not in initial_repo_dict and removes them. |
|
458 | that are not in initial_repo_dict and removes them. | |
459 |
|
459 | |||
460 | :param initial_repo_dict: mapping with repositories found by scanning methods |
|
460 | :param initial_repo_dict: mapping with repositories found by scanning methods | |
461 | :param remove_obsolete: check for obsolete entries in database |
|
461 | :param remove_obsolete: check for obsolete entries in database | |
462 | :param install_git_hooks: if this is True, also check and install git hook |
|
462 | :param install_git_hooks: if this is True, also check and install git hook | |
463 | for a repo if missing |
|
463 | for a repo if missing | |
464 | :param overwrite_git_hooks: if this is True, overwrite any existing git hooks |
|
464 | :param overwrite_git_hooks: if this is True, overwrite any existing git hooks | |
465 | that may be encountered (even if user-deployed) |
|
465 | that may be encountered (even if user-deployed) | |
466 | """ |
|
466 | """ | |
467 | from kallithea.model.repo import RepoModel |
|
467 | from kallithea.model.repo import RepoModel | |
468 | from kallithea.model.scm import ScmModel |
|
468 | from kallithea.model.scm import ScmModel | |
469 | sa = meta.Session() |
|
469 | sa = meta.Session() | |
470 | repo_model = RepoModel() |
|
470 | repo_model = RepoModel() | |
471 | if user is None: |
|
471 | if user is None: | |
472 | user = User.get_first_admin() |
|
472 | user = User.get_first_admin() | |
473 | added = [] |
|
473 | added = [] | |
474 |
|
474 | |||
475 | # creation defaults |
|
475 | # creation defaults | |
476 | defs = Setting.get_default_repo_settings(strip_prefix=True) |
|
476 | defs = Setting.get_default_repo_settings(strip_prefix=True) | |
477 | enable_statistics = defs.get('repo_enable_statistics') |
|
477 | enable_statistics = defs.get('repo_enable_statistics') | |
478 | enable_downloads = defs.get('repo_enable_downloads') |
|
478 | enable_downloads = defs.get('repo_enable_downloads') | |
479 | private = defs.get('repo_private') |
|
479 | private = defs.get('repo_private') | |
480 |
|
480 | |||
481 | for name, repo in initial_repo_dict.items(): |
|
481 | for name, repo in initial_repo_dict.items(): | |
482 | group = map_groups(name) |
|
482 | group = map_groups(name) | |
483 | db_repo = repo_model.get_by_repo_name(name) |
|
483 | db_repo = repo_model.get_by_repo_name(name) | |
484 | # found repo that is on filesystem not in Kallithea database |
|
484 | # found repo that is on filesystem not in Kallithea database | |
485 | if not db_repo: |
|
485 | if not db_repo: | |
486 | log.info('repository %s not found, creating now', name) |
|
486 | log.info('repository %s not found, creating now', name) | |
487 | added.append(name) |
|
487 | added.append(name) | |
488 | desc = (repo.description |
|
488 | desc = (repo.description | |
489 | if repo.description != 'unknown' |
|
489 | if repo.description != 'unknown' | |
490 | else '%s repository' % name) |
|
490 | else '%s repository' % name) | |
491 |
|
491 | |||
492 | new_repo = repo_model._create_repo( |
|
492 | new_repo = repo_model._create_repo( | |
493 | repo_name=name, |
|
493 | repo_name=name, | |
494 | repo_type=repo.alias, |
|
494 | repo_type=repo.alias, | |
495 | description=desc, |
|
495 | description=desc, | |
496 | repo_group=getattr(group, 'group_id', None), |
|
496 | repo_group=getattr(group, 'group_id', None), | |
497 | owner=user, |
|
497 | owner=user, | |
498 | enable_downloads=enable_downloads, |
|
498 | enable_downloads=enable_downloads, | |
499 | enable_statistics=enable_statistics, |
|
499 | enable_statistics=enable_statistics, | |
500 | private=private, |
|
500 | private=private, | |
501 | state=Repository.STATE_CREATED |
|
501 | state=Repository.STATE_CREATED | |
502 | ) |
|
502 | ) | |
503 | sa.commit() |
|
503 | sa.commit() | |
504 | # we added that repo just now, and make sure it has githook |
|
504 | # we added that repo just now, and make sure it has githook | |
505 | # installed, and updated server info |
|
505 | # installed, and updated server info | |
506 | if new_repo.repo_type == 'git': |
|
506 | if new_repo.repo_type == 'git': | |
507 | git_repo = new_repo.scm_instance |
|
507 | git_repo = new_repo.scm_instance | |
508 | ScmModel().install_git_hooks(git_repo) |
|
508 | ScmModel().install_git_hooks(git_repo) | |
509 | # update repository server-info |
|
509 | # update repository server-info | |
510 | log.debug('Running update server info') |
|
510 | log.debug('Running update server info') | |
511 | git_repo._update_server_info() |
|
511 | git_repo._update_server_info() | |
512 | new_repo.update_changeset_cache() |
|
512 | new_repo.update_changeset_cache() | |
513 | elif install_git_hooks: |
|
513 | elif install_git_hooks: | |
514 | if db_repo.repo_type == 'git': |
|
514 | if db_repo.repo_type == 'git': | |
515 | ScmModel().install_git_hooks(db_repo.scm_instance, force_create=overwrite_git_hooks) |
|
515 | ScmModel().install_git_hooks(db_repo.scm_instance, force_create=overwrite_git_hooks) | |
516 |
|
516 | |||
517 | removed = [] |
|
517 | removed = [] | |
518 | # remove from database those repositories that are not in the filesystem |
|
518 | # remove from database those repositories that are not in the filesystem | |
519 | for repo in sa.query(Repository).all(): |
|
519 | for repo in sa.query(Repository).all(): | |
520 | if repo.repo_name not in initial_repo_dict: |
|
520 | if repo.repo_name not in initial_repo_dict: | |
521 | if remove_obsolete: |
|
521 | if remove_obsolete: | |
522 | log.debug("Removing non-existing repository found in db `%s`", |
|
522 | log.debug("Removing non-existing repository found in db `%s`", | |
523 | repo.repo_name) |
|
523 | repo.repo_name) | |
524 | try: |
|
524 | try: | |
525 | RepoModel().delete(repo, forks='detach', fs_remove=False) |
|
525 | RepoModel().delete(repo, forks='detach', fs_remove=False) | |
526 | sa.commit() |
|
526 | sa.commit() | |
527 | except Exception: |
|
527 | except Exception: | |
528 | #don't hold further removals on error |
|
528 | #don't hold further removals on error | |
529 | log.error(traceback.format_exc()) |
|
529 | log.error(traceback.format_exc()) | |
530 | sa.rollback() |
|
530 | sa.rollback() | |
531 | removed.append(repo.repo_name) |
|
531 | removed.append(repo.repo_name) | |
532 | return added, removed |
|
532 | return added, removed | |
533 |
|
533 | |||
534 |
|
534 | |||
535 | def load_rcextensions(root_path): |
|
535 | def load_rcextensions(root_path): | |
536 | path = os.path.join(root_path, 'rcextensions', '__init__.py') |
|
536 | path = os.path.join(root_path, 'rcextensions', '__init__.py') | |
537 | if os.path.isfile(path): |
|
537 | if os.path.isfile(path): | |
538 | rcext = create_module('rc', path) |
|
538 | rcext = create_module('rc', path) | |
539 | EXT = kallithea.EXTENSIONS = rcext |
|
539 | EXT = kallithea.EXTENSIONS = rcext | |
540 | log.debug('Found rcextensions now loading %s...', rcext) |
|
540 | log.debug('Found rcextensions now loading %s...', rcext) | |
541 |
|
541 | |||
542 | # Additional mappings that are not present in the pygments lexers |
|
542 | # Additional mappings that are not present in the pygments lexers | |
543 | kallithea.config.conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {})) |
|
543 | kallithea.config.conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {})) | |
544 |
|
544 | |||
545 | # OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present) |
|
545 | # OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present) | |
546 |
|
546 | |||
547 | if getattr(EXT, 'INDEX_EXTENSIONS', []): |
|
547 | if getattr(EXT, 'INDEX_EXTENSIONS', []): | |
548 | log.debug('settings custom INDEX_EXTENSIONS') |
|
548 | log.debug('settings custom INDEX_EXTENSIONS') | |
549 | kallithea.config.conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', []) |
|
549 | kallithea.config.conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', []) | |
550 |
|
550 | |||
551 | # ADDITIONAL MAPPINGS |
|
551 | # ADDITIONAL MAPPINGS | |
552 | log.debug('adding extra into INDEX_EXTENSIONS') |
|
552 | log.debug('adding extra into INDEX_EXTENSIONS') | |
553 | kallithea.config.conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', [])) |
|
553 | kallithea.config.conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', [])) | |
554 |
|
554 | |||
555 | # auto check if the module is not missing any data, set to default if is |
|
555 | # auto check if the module is not missing any data, set to default if is | |
556 | # this will help autoupdate new feature of rcext module |
|
556 | # this will help autoupdate new feature of rcext module | |
557 | #from kallithea.config import rcextensions |
|
557 | #from kallithea.config import rcextensions | |
558 | #for k in dir(rcextensions): |
|
558 | #for k in dir(rcextensions): | |
559 | # if not k.startswith('_') and not hasattr(EXT, k): |
|
559 | # if not k.startswith('_') and not hasattr(EXT, k): | |
560 | # setattr(EXT, k, getattr(rcextensions, k)) |
|
560 | # setattr(EXT, k, getattr(rcextensions, k)) | |
561 |
|
561 | |||
562 |
|
562 | |||
563 | #============================================================================== |
|
563 | #============================================================================== | |
564 | # MISC |
|
564 | # MISC | |
565 | #============================================================================== |
|
565 | #============================================================================== | |
566 |
|
566 | |||
567 | git_req_ver = StrictVersion('1.7.4') |
|
567 | git_req_ver = StrictVersion('1.7.4') | |
568 |
|
568 | |||
569 | def check_git_version(): |
|
569 | def check_git_version(): | |
570 | """ |
|
570 | """ | |
571 | Checks what version of git is installed on the system, and raise a system exit |
|
571 | Checks what version of git is installed on the system, and raise a system exit | |
572 | if it's too old for Kallithea to work properly. |
|
572 | if it's too old for Kallithea to work properly. | |
573 | """ |
|
573 | """ | |
574 | if 'git' not in kallithea.BACKENDS: |
|
574 | if 'git' not in kallithea.BACKENDS: | |
575 | return None |
|
575 | return None | |
576 |
|
576 | |||
577 | if not settings.GIT_EXECUTABLE_PATH: |
|
577 | if not settings.GIT_EXECUTABLE_PATH: | |
578 | log.warning('No git executable configured - check "git_path" in the ini file.') |
|
578 | log.warning('No git executable configured - check "git_path" in the ini file.') | |
579 | return None |
|
579 | return None | |
580 |
|
580 | |||
581 | try: |
|
581 | try: | |
582 | stdout, stderr = GitRepository._run_git_command(['--version']) |
|
582 | stdout, stderr = GitRepository._run_git_command(['--version']) | |
583 | except RepositoryError as e: |
|
583 | except RepositoryError as e: | |
584 | # message will already have been logged as error |
|
584 | # message will already have been logged as error | |
585 | log.warning('No working git executable found - check "git_path" in the ini file.') |
|
585 | log.warning('No working git executable found - check "git_path" in the ini file.') | |
586 | return None |
|
586 | return None | |
587 |
|
587 | |||
588 | if stderr: |
|
588 | if stderr: | |
589 | log.warning('Error/stderr from "%s --version":\n%s', settings.GIT_EXECUTABLE_PATH, stderr) |
|
589 | log.warning('Error/stderr from "%s --version":\n%s', settings.GIT_EXECUTABLE_PATH, safe_str(stderr)) | |
590 |
|
590 | |||
591 | if not stdout: |
|
591 | if not stdout: | |
592 | log.warning('No working git executable found - check "git_path" in the ini file.') |
|
592 | log.warning('No working git executable found - check "git_path" in the ini file.') | |
593 | return None |
|
593 | return None | |
594 |
|
594 | |||
595 | output = stdout.strip() |
|
595 | output = safe_str(stdout).strip() | |
596 | m = re.search(r"\d+.\d+.\d+", output) |
|
596 | m = re.search(r"\d+.\d+.\d+", output) | |
597 | if m: |
|
597 | if m: | |
598 | ver = StrictVersion(m.group(0)) |
|
598 | ver = StrictVersion(m.group(0)) | |
599 | log.debug('Git executable: "%s", version %s (parsed from: "%s")', |
|
599 | log.debug('Git executable: "%s", version %s (parsed from: "%s")', | |
600 | settings.GIT_EXECUTABLE_PATH, ver, output) |
|
600 | settings.GIT_EXECUTABLE_PATH, ver, output) | |
601 | if ver < git_req_ver: |
|
601 | if ver < git_req_ver: | |
602 | log.error('Kallithea detected %s version %s, which is too old ' |
|
602 | log.error('Kallithea detected %s version %s, which is too old ' | |
603 | 'for the system to function properly. ' |
|
603 | 'for the system to function properly. ' | |
604 | 'Please upgrade to version %s or later. ' |
|
604 | 'Please upgrade to version %s or later. ' | |
605 | 'If you strictly need Mercurial repositories, you can ' |
|
605 | 'If you strictly need Mercurial repositories, you can ' | |
606 | 'clear the "git_path" setting in the ini file.', |
|
606 | 'clear the "git_path" setting in the ini file.', | |
607 | settings.GIT_EXECUTABLE_PATH, ver, git_req_ver) |
|
607 | settings.GIT_EXECUTABLE_PATH, ver, git_req_ver) | |
608 | log.error("Terminating ...") |
|
608 | log.error("Terminating ...") | |
609 | sys.exit(1) |
|
609 | sys.exit(1) | |
610 | else: |
|
610 | else: | |
611 | ver = StrictVersion('0.0.0') |
|
611 | ver = StrictVersion('0.0.0') | |
612 | log.warning('Error finding version number in "%s --version" stdout:\n%s', |
|
612 | log.warning('Error finding version number in "%s --version" stdout:\n%s', | |
613 | settings.GIT_EXECUTABLE_PATH, output) |
|
613 | settings.GIT_EXECUTABLE_PATH, output) | |
614 |
|
614 | |||
615 | return ver |
|
615 | return ver | |
616 |
|
616 | |||
617 |
|
617 | |||
618 | #=============================================================================== |
|
618 | #=============================================================================== | |
619 | # CACHE RELATED METHODS |
|
619 | # CACHE RELATED METHODS | |
620 | #=============================================================================== |
|
620 | #=============================================================================== | |
621 |
|
621 | |||
622 | def conditional_cache(region, prefix, condition, func): |
|
622 | def conditional_cache(region, prefix, condition, func): | |
623 | """ |
|
623 | """ | |
624 |
|
624 | |||
625 | Conditional caching function use like:: |
|
625 | Conditional caching function use like:: | |
626 | def _c(arg): |
|
626 | def _c(arg): | |
627 | #heavy computation function |
|
627 | #heavy computation function | |
628 | return data |
|
628 | return data | |
629 |
|
629 | |||
630 | # depending from condition the compute is wrapped in cache or not |
|
630 | # depending from condition the compute is wrapped in cache or not | |
631 | compute = conditional_cache('short_term', 'cache_desc', condition=True, func=func) |
|
631 | compute = conditional_cache('short_term', 'cache_desc', condition=True, func=func) | |
632 | return compute(arg) |
|
632 | return compute(arg) | |
633 |
|
633 | |||
634 | :param region: name of cache region |
|
634 | :param region: name of cache region | |
635 | :param prefix: cache region prefix |
|
635 | :param prefix: cache region prefix | |
636 | :param condition: condition for cache to be triggered, and return data cached |
|
636 | :param condition: condition for cache to be triggered, and return data cached | |
637 | :param func: wrapped heavy function to compute |
|
637 | :param func: wrapped heavy function to compute | |
638 |
|
638 | |||
639 | """ |
|
639 | """ | |
640 | wrapped = func |
|
640 | wrapped = func | |
641 | if condition: |
|
641 | if condition: | |
642 | log.debug('conditional_cache: True, wrapping call of ' |
|
642 | log.debug('conditional_cache: True, wrapping call of ' | |
643 | 'func: %s into %s region cache' % (region, func)) |
|
643 | 'func: %s into %s region cache' % (region, func)) | |
644 | wrapped = beaker.cache._cache_decorate((prefix,), None, None, region)(func) |
|
644 | wrapped = beaker.cache._cache_decorate((prefix,), None, None, region)(func) | |
645 |
|
645 | |||
646 | return wrapped |
|
646 | return wrapped |
@@ -1,552 +1,550 b'' | |||||
1 | import re |
|
1 | import re | |
2 | from io import BytesIO |
|
2 | from io import BytesIO | |
3 | from itertools import chain |
|
3 | from itertools import chain | |
4 | from subprocess import PIPE, Popen |
|
4 | from subprocess import PIPE, Popen | |
5 |
|
5 | |||
6 | from dulwich import objects |
|
6 | from dulwich import objects | |
7 | from dulwich.config import ConfigFile |
|
7 | from dulwich.config import ConfigFile | |
8 |
|
8 | |||
9 | from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset |
|
9 | from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset | |
10 | from kallithea.lib.vcs.conf import settings |
|
10 | from kallithea.lib.vcs.conf import settings | |
11 | from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, RepositoryError, VCSError |
|
11 | from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, RepositoryError, VCSError | |
12 | from kallithea.lib.vcs.nodes import ( |
|
12 | from kallithea.lib.vcs.nodes import ( | |
13 | AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode) |
|
13 | AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode) | |
14 | from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, date_fromtimestamp, safe_int, safe_str |
|
14 | from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, date_fromtimestamp, safe_int, safe_str | |
15 | from kallithea.lib.vcs.utils.lazy import LazyProperty |
|
15 | from kallithea.lib.vcs.utils.lazy import LazyProperty | |
16 |
|
16 | |||
17 |
|
17 | |||
18 | class GitChangeset(BaseChangeset): |
|
18 | class GitChangeset(BaseChangeset): | |
19 | """ |
|
19 | """ | |
20 | Represents state of the repository at a revision. |
|
20 | Represents state of the repository at a revision. | |
21 | """ |
|
21 | """ | |
22 |
|
22 | |||
23 | def __init__(self, repository, revision): |
|
23 | def __init__(self, repository, revision): | |
24 | self._stat_modes = {} |
|
24 | self._stat_modes = {} | |
25 | self.repository = repository |
|
25 | self.repository = repository | |
26 | try: |
|
26 | try: | |
27 | commit = self.repository._repo[ascii_bytes(revision)] |
|
27 | commit = self.repository._repo[ascii_bytes(revision)] | |
28 | if isinstance(commit, objects.Tag): |
|
28 | if isinstance(commit, objects.Tag): | |
29 | revision = safe_str(commit.object[1]) |
|
29 | revision = safe_str(commit.object[1]) | |
30 | commit = self.repository._repo.get_object(commit.object[1]) |
|
30 | commit = self.repository._repo.get_object(commit.object[1]) | |
31 | except KeyError: |
|
31 | except KeyError: | |
32 | raise RepositoryError("Cannot get object with id %s" % revision) |
|
32 | raise RepositoryError("Cannot get object with id %s" % revision) | |
33 | self.raw_id = ascii_str(commit.id) |
|
33 | self.raw_id = ascii_str(commit.id) | |
34 | self.short_id = self.raw_id[:12] |
|
34 | self.short_id = self.raw_id[:12] | |
35 | self._commit = commit # a Dulwich Commmit with .id |
|
35 | self._commit = commit # a Dulwich Commmit with .id | |
36 | self._tree_id = commit.tree |
|
36 | self._tree_id = commit.tree | |
37 | self._committer_property = 'committer' |
|
37 | self._committer_property = 'committer' | |
38 | self._author_property = 'author' |
|
38 | self._author_property = 'author' | |
39 | self._date_property = 'commit_time' |
|
39 | self._date_property = 'commit_time' | |
40 | self._date_tz_property = 'commit_timezone' |
|
40 | self._date_tz_property = 'commit_timezone' | |
41 | self.revision = repository.revisions.index(self.raw_id) |
|
41 | self.revision = repository.revisions.index(self.raw_id) | |
42 |
|
42 | |||
43 | self.nodes = {} |
|
43 | self.nodes = {} | |
44 | self._paths = {} |
|
44 | self._paths = {} | |
45 |
|
45 | |||
46 | @LazyProperty |
|
46 | @LazyProperty | |
47 | def bookmarks(self): |
|
47 | def bookmarks(self): | |
48 | return () |
|
48 | return () | |
49 |
|
49 | |||
50 | @LazyProperty |
|
50 | @LazyProperty | |
51 | def message(self): |
|
51 | def message(self): | |
52 | return safe_str(self._commit.message) |
|
52 | return safe_str(self._commit.message) | |
53 |
|
53 | |||
54 | @LazyProperty |
|
54 | @LazyProperty | |
55 | def committer(self): |
|
55 | def committer(self): | |
56 | return safe_str(getattr(self._commit, self._committer_property)) |
|
56 | return safe_str(getattr(self._commit, self._committer_property)) | |
57 |
|
57 | |||
58 | @LazyProperty |
|
58 | @LazyProperty | |
59 | def author(self): |
|
59 | def author(self): | |
60 | return safe_str(getattr(self._commit, self._author_property)) |
|
60 | return safe_str(getattr(self._commit, self._author_property)) | |
61 |
|
61 | |||
62 | @LazyProperty |
|
62 | @LazyProperty | |
63 | def date(self): |
|
63 | def date(self): | |
64 | return date_fromtimestamp(getattr(self._commit, self._date_property), |
|
64 | return date_fromtimestamp(getattr(self._commit, self._date_property), | |
65 | getattr(self._commit, self._date_tz_property)) |
|
65 | getattr(self._commit, self._date_tz_property)) | |
66 |
|
66 | |||
67 | @LazyProperty |
|
67 | @LazyProperty | |
68 | def _timestamp(self): |
|
68 | def _timestamp(self): | |
69 | return getattr(self._commit, self._date_property) |
|
69 | return getattr(self._commit, self._date_property) | |
70 |
|
70 | |||
71 | @LazyProperty |
|
71 | @LazyProperty | |
72 | def status(self): |
|
72 | def status(self): | |
73 | """ |
|
73 | """ | |
74 | Returns modified, added, removed, deleted files for current changeset |
|
74 | Returns modified, added, removed, deleted files for current changeset | |
75 | """ |
|
75 | """ | |
76 | return self.changed, self.added, self.removed |
|
76 | return self.changed, self.added, self.removed | |
77 |
|
77 | |||
78 | @LazyProperty |
|
78 | @LazyProperty | |
79 | def tags(self): |
|
79 | def tags(self): | |
80 | _tags = [] |
|
80 | _tags = [] | |
81 | for tname, tsha in self.repository.tags.items(): |
|
81 | for tname, tsha in self.repository.tags.items(): | |
82 | if tsha == self.raw_id: |
|
82 | if tsha == self.raw_id: | |
83 | _tags.append(tname) |
|
83 | _tags.append(tname) | |
84 | return _tags |
|
84 | return _tags | |
85 |
|
85 | |||
86 | @LazyProperty |
|
86 | @LazyProperty | |
87 | def branch(self): |
|
87 | def branch(self): | |
88 | # Note: This function will return one branch name for the changeset - |
|
88 | # Note: This function will return one branch name for the changeset - | |
89 | # that might not make sense in Git where branches() is a better match |
|
89 | # that might not make sense in Git where branches() is a better match | |
90 | # for the basic model |
|
90 | # for the basic model | |
91 | heads = self.repository._heads(reverse=False) |
|
91 | heads = self.repository._heads(reverse=False) | |
92 | ref = heads.get(self._commit.id) |
|
92 | ref = heads.get(self._commit.id) | |
93 | if ref: |
|
93 | if ref: | |
94 | return safe_str(ref) |
|
94 | return safe_str(ref) | |
95 |
|
95 | |||
96 | @LazyProperty |
|
96 | @LazyProperty | |
97 | def branches(self): |
|
97 | def branches(self): | |
98 | heads = self.repository._heads(reverse=True) |
|
98 | heads = self.repository._heads(reverse=True) | |
99 | return [b for b in heads if heads[b] == self._commit.id] # FIXME: Inefficient ... and returning None! |
|
99 | return [safe_str(b) for b in heads if heads[b] == self._commit.id] # FIXME: Inefficient ... and returning None! | |
100 |
|
100 | |||
101 | def _fix_path(self, path): |
|
101 | def _fix_path(self, path): | |
102 | """ |
|
102 | """ | |
103 | Paths are stored without trailing slash so we need to get rid off it if |
|
103 | Paths are stored without trailing slash so we need to get rid off it if | |
104 | needed. |
|
104 | needed. | |
105 | """ |
|
105 | """ | |
106 | if path.endswith('/'): |
|
106 | if path.endswith('/'): | |
107 | path = path.rstrip('/') |
|
107 | path = path.rstrip('/') | |
108 | return path |
|
108 | return path | |
109 |
|
109 | |||
110 | def _get_id_for_path(self, path): |
|
110 | def _get_id_for_path(self, path): | |
111 | # FIXME: Please, spare a couple of minutes and make those codes cleaner; |
|
111 | # FIXME: Please, spare a couple of minutes and make those codes cleaner; | |
112 | if path not in self._paths: |
|
112 | if path not in self._paths: | |
113 | path = path.strip('/') |
|
113 | path = path.strip('/') | |
114 | # set root tree |
|
114 | # set root tree | |
115 | tree = self.repository._repo[self._tree_id] |
|
115 | tree = self.repository._repo[self._tree_id] | |
116 | if path == '': |
|
116 | if path == '': | |
117 | self._paths[''] = tree.id |
|
117 | self._paths[''] = tree.id | |
118 | return tree.id |
|
118 | return tree.id | |
119 | splitted = path.split('/') |
|
119 | splitted = path.split('/') | |
120 | dirs, name = splitted[:-1], splitted[-1] |
|
120 | dirs, name = splitted[:-1], splitted[-1] | |
121 | curdir = '' |
|
121 | curdir = '' | |
122 |
|
122 | |||
123 | # initially extract things from root dir |
|
123 | # initially extract things from root dir | |
124 | for item, stat, id in tree.items(): |
|
124 | for item, stat, id in tree.items(): | |
|
125 | name = safe_str(item) | |||
125 | if curdir: |
|
126 | if curdir: | |
126 |
name = '/'.join((curdir, |
|
127 | name = '/'.join((curdir, name)) | |
127 | else: |
|
|||
128 | name = item |
|
|||
129 | self._paths[name] = id |
|
128 | self._paths[name] = id | |
130 | self._stat_modes[name] = stat |
|
129 | self._stat_modes[name] = stat | |
131 |
|
130 | |||
132 | for dir in dirs: |
|
131 | for dir in dirs: | |
133 | if curdir: |
|
132 | if curdir: | |
134 | curdir = '/'.join((curdir, dir)) |
|
133 | curdir = '/'.join((curdir, dir)) | |
135 | else: |
|
134 | else: | |
136 | curdir = dir |
|
135 | curdir = dir | |
137 | dir_id = None |
|
136 | dir_id = None | |
138 | for item, stat, id in tree.items(): |
|
137 | for item, stat, id in tree.items(): | |
139 |
|
|
138 | name = safe_str(item) | |
|
139 | if dir == name: | |||
140 | dir_id = id |
|
140 | dir_id = id | |
141 | if dir_id: |
|
141 | if dir_id: | |
142 | # Update tree |
|
142 | # Update tree | |
143 | tree = self.repository._repo[dir_id] |
|
143 | tree = self.repository._repo[dir_id] | |
144 | if not isinstance(tree, objects.Tree): |
|
144 | if not isinstance(tree, objects.Tree): | |
145 | raise ChangesetError('%s is not a directory' % curdir) |
|
145 | raise ChangesetError('%s is not a directory' % curdir) | |
146 | else: |
|
146 | else: | |
147 | raise ChangesetError('%s have not been found' % curdir) |
|
147 | raise ChangesetError('%s have not been found' % curdir) | |
148 |
|
148 | |||
149 | # cache all items from the given traversed tree |
|
149 | # cache all items from the given traversed tree | |
150 | for item, stat, id in tree.items(): |
|
150 | for item, stat, id in tree.items(): | |
|
151 | name = safe_str(item) | |||
151 | if curdir: |
|
152 | if curdir: | |
152 |
name = '/'.join((curdir, |
|
153 | name = '/'.join((curdir, name)) | |
153 | else: |
|
|||
154 | name = item |
|
|||
155 | self._paths[name] = id |
|
154 | self._paths[name] = id | |
156 | self._stat_modes[name] = stat |
|
155 | self._stat_modes[name] = stat | |
157 | if path not in self._paths: |
|
156 | if path not in self._paths: | |
158 | raise NodeDoesNotExistError("There is no file nor directory " |
|
157 | raise NodeDoesNotExistError("There is no file nor directory " | |
159 | "at the given path '%s' at revision %s" |
|
158 | "at the given path '%s' at revision %s" | |
160 | % (path, self.short_id)) |
|
159 | % (path, self.short_id)) | |
161 | return self._paths[path] |
|
160 | return self._paths[path] | |
162 |
|
161 | |||
163 | def _get_kind(self, path): |
|
162 | def _get_kind(self, path): | |
164 | obj = self.repository._repo[self._get_id_for_path(path)] |
|
163 | obj = self.repository._repo[self._get_id_for_path(path)] | |
165 | if isinstance(obj, objects.Blob): |
|
164 | if isinstance(obj, objects.Blob): | |
166 | return NodeKind.FILE |
|
165 | return NodeKind.FILE | |
167 | elif isinstance(obj, objects.Tree): |
|
166 | elif isinstance(obj, objects.Tree): | |
168 | return NodeKind.DIR |
|
167 | return NodeKind.DIR | |
169 |
|
168 | |||
170 | def _get_filectx(self, path): |
|
169 | def _get_filectx(self, path): | |
171 | path = self._fix_path(path) |
|
170 | path = self._fix_path(path) | |
172 | if self._get_kind(path) != NodeKind.FILE: |
|
171 | if self._get_kind(path) != NodeKind.FILE: | |
173 | raise ChangesetError("File does not exist for revision %s at " |
|
172 | raise ChangesetError("File does not exist for revision %s at " | |
174 | " '%s'" % (self.raw_id, path)) |
|
173 | " '%s'" % (self.raw_id, path)) | |
175 | return path |
|
174 | return path | |
176 |
|
175 | |||
177 | def _get_file_nodes(self): |
|
176 | def _get_file_nodes(self): | |
178 | return chain(*(t[2] for t in self.walk())) |
|
177 | return chain(*(t[2] for t in self.walk())) | |
179 |
|
178 | |||
180 | @LazyProperty |
|
179 | @LazyProperty | |
181 | def parents(self): |
|
180 | def parents(self): | |
182 | """ |
|
181 | """ | |
183 | Returns list of parents changesets. |
|
182 | Returns list of parents changesets. | |
184 | """ |
|
183 | """ | |
185 | return [self.repository.get_changeset(ascii_str(parent_id)) |
|
184 | return [self.repository.get_changeset(ascii_str(parent_id)) | |
186 | for parent_id in self._commit.parents] |
|
185 | for parent_id in self._commit.parents] | |
187 |
|
186 | |||
188 | @LazyProperty |
|
187 | @LazyProperty | |
189 | def children(self): |
|
188 | def children(self): | |
190 | """ |
|
189 | """ | |
191 | Returns list of children changesets. |
|
190 | Returns list of children changesets. | |
192 | """ |
|
191 | """ | |
193 | rev_filter = settings.GIT_REV_FILTER |
|
192 | rev_filter = settings.GIT_REV_FILTER | |
194 | so = self.repository.run_git_command( |
|
193 | so = self.repository.run_git_command( | |
195 | ['rev-list', rev_filter, '--children'] |
|
194 | ['rev-list', rev_filter, '--children'] | |
196 | ) |
|
195 | ) | |
197 | return [ |
|
196 | return [ | |
198 | self.repository.get_changeset(cs) |
|
197 | self.repository.get_changeset(cs) | |
199 | for parts in (l.split(' ') for l in so.splitlines()) |
|
198 | for parts in (l.split(' ') for l in so.splitlines()) | |
200 | if parts[0] == self.raw_id |
|
199 | if parts[0] == self.raw_id | |
201 | for cs in parts[1:] |
|
200 | for cs in parts[1:] | |
202 | ] |
|
201 | ] | |
203 |
|
202 | |||
204 | def next(self, branch=None): |
|
203 | def next(self, branch=None): | |
205 | if branch and self.branch != branch: |
|
204 | if branch and self.branch != branch: | |
206 | raise VCSError('Branch option used on changeset not belonging ' |
|
205 | raise VCSError('Branch option used on changeset not belonging ' | |
207 | 'to that branch') |
|
206 | 'to that branch') | |
208 |
|
207 | |||
209 | cs = self |
|
208 | cs = self | |
210 | while True: |
|
209 | while True: | |
211 | try: |
|
210 | try: | |
212 | next_ = cs.revision + 1 |
|
211 | next_ = cs.revision + 1 | |
213 | next_rev = cs.repository.revisions[next_] |
|
212 | next_rev = cs.repository.revisions[next_] | |
214 | except IndexError: |
|
213 | except IndexError: | |
215 | raise ChangesetDoesNotExistError |
|
214 | raise ChangesetDoesNotExistError | |
216 | cs = cs.repository.get_changeset(next_rev) |
|
215 | cs = cs.repository.get_changeset(next_rev) | |
217 |
|
216 | |||
218 | if not branch or branch == cs.branch: |
|
217 | if not branch or branch == cs.branch: | |
219 | return cs |
|
218 | return cs | |
220 |
|
219 | |||
221 | def prev(self, branch=None): |
|
220 | def prev(self, branch=None): | |
222 | if branch and self.branch != branch: |
|
221 | if branch and self.branch != branch: | |
223 | raise VCSError('Branch option used on changeset not belonging ' |
|
222 | raise VCSError('Branch option used on changeset not belonging ' | |
224 | 'to that branch') |
|
223 | 'to that branch') | |
225 |
|
224 | |||
226 | cs = self |
|
225 | cs = self | |
227 | while True: |
|
226 | while True: | |
228 | try: |
|
227 | try: | |
229 | prev_ = cs.revision - 1 |
|
228 | prev_ = cs.revision - 1 | |
230 | if prev_ < 0: |
|
229 | if prev_ < 0: | |
231 | raise IndexError |
|
230 | raise IndexError | |
232 | prev_rev = cs.repository.revisions[prev_] |
|
231 | prev_rev = cs.repository.revisions[prev_] | |
233 | except IndexError: |
|
232 | except IndexError: | |
234 | raise ChangesetDoesNotExistError |
|
233 | raise ChangesetDoesNotExistError | |
235 | cs = cs.repository.get_changeset(prev_rev) |
|
234 | cs = cs.repository.get_changeset(prev_rev) | |
236 |
|
235 | |||
237 | if not branch or branch == cs.branch: |
|
236 | if not branch or branch == cs.branch: | |
238 | return cs |
|
237 | return cs | |
239 |
|
238 | |||
240 | def diff(self, ignore_whitespace=True, context=3): |
|
239 | def diff(self, ignore_whitespace=True, context=3): | |
241 | # Only used to feed diffstat |
|
240 | # Only used to feed diffstat | |
242 | rev1 = self.parents[0] if self.parents else self.repository.EMPTY_CHANGESET |
|
241 | rev1 = self.parents[0] if self.parents else self.repository.EMPTY_CHANGESET | |
243 | rev2 = self |
|
242 | rev2 = self | |
244 | return b''.join(self.repository.get_diff(rev1, rev2, |
|
243 | return b''.join(self.repository.get_diff(rev1, rev2, | |
245 | ignore_whitespace=ignore_whitespace, |
|
244 | ignore_whitespace=ignore_whitespace, | |
246 | context=context)) |
|
245 | context=context)) | |
247 |
|
246 | |||
248 | def get_file_mode(self, path): |
|
247 | def get_file_mode(self, path): | |
249 | """ |
|
248 | """ | |
250 | Returns stat mode of the file at the given ``path``. |
|
249 | Returns stat mode of the file at the given ``path``. | |
251 | """ |
|
250 | """ | |
252 | # ensure path is traversed |
|
251 | # ensure path is traversed | |
253 | self._get_id_for_path(path) |
|
252 | self._get_id_for_path(path) | |
254 | return self._stat_modes[path] |
|
253 | return self._stat_modes[path] | |
255 |
|
254 | |||
256 | def get_file_content(self, path): |
|
255 | def get_file_content(self, path): | |
257 | """ |
|
256 | """ | |
258 | Returns content of the file at given ``path``. |
|
257 | Returns content of the file at given ``path``. | |
259 | """ |
|
258 | """ | |
260 | id = self._get_id_for_path(path) |
|
259 | id = self._get_id_for_path(path) | |
261 | blob = self.repository._repo[id] |
|
260 | blob = self.repository._repo[id] | |
262 | return blob.as_pretty_string() |
|
261 | return blob.as_pretty_string() | |
263 |
|
262 | |||
264 | def get_file_size(self, path): |
|
263 | def get_file_size(self, path): | |
265 | """ |
|
264 | """ | |
266 | Returns size of the file at given ``path``. |
|
265 | Returns size of the file at given ``path``. | |
267 | """ |
|
266 | """ | |
268 | id = self._get_id_for_path(path) |
|
267 | id = self._get_id_for_path(path) | |
269 | blob = self.repository._repo[id] |
|
268 | blob = self.repository._repo[id] | |
270 | return blob.raw_length() |
|
269 | return blob.raw_length() | |
271 |
|
270 | |||
272 | def get_file_changeset(self, path): |
|
271 | def get_file_changeset(self, path): | |
273 | """ |
|
272 | """ | |
274 | Returns last commit of the file at the given ``path``. |
|
273 | Returns last commit of the file at the given ``path``. | |
275 | """ |
|
274 | """ | |
276 | return self.get_file_history(path, limit=1)[0] |
|
275 | return self.get_file_history(path, limit=1)[0] | |
277 |
|
276 | |||
278 | def get_file_history(self, path, limit=None): |
|
277 | def get_file_history(self, path, limit=None): | |
279 | """ |
|
278 | """ | |
280 | Returns history of file as reversed list of ``Changeset`` objects for |
|
279 | Returns history of file as reversed list of ``Changeset`` objects for | |
281 | which file at given ``path`` has been modified. |
|
280 | which file at given ``path`` has been modified. | |
282 |
|
281 | |||
283 | TODO: This function now uses os underlying 'git' and 'grep' commands |
|
282 | TODO: This function now uses os underlying 'git' and 'grep' commands | |
284 | which is generally not good. Should be replaced with algorithm |
|
283 | which is generally not good. Should be replaced with algorithm | |
285 | iterating commits. |
|
284 | iterating commits. | |
286 | """ |
|
285 | """ | |
287 | self._get_filectx(path) |
|
286 | self._get_filectx(path) | |
288 |
|
287 | |||
289 | if limit is not None: |
|
288 | if limit is not None: | |
290 | cmd = ['log', '-n', str(safe_int(limit, 0)), |
|
289 | cmd = ['log', '-n', str(safe_int(limit, 0)), | |
291 | '--pretty=format:%H', '-s', self.raw_id, '--', path] |
|
290 | '--pretty=format:%H', '-s', self.raw_id, '--', path] | |
292 |
|
291 | |||
293 | else: |
|
292 | else: | |
294 | cmd = ['log', |
|
293 | cmd = ['log', | |
295 | '--pretty=format:%H', '-s', self.raw_id, '--', path] |
|
294 | '--pretty=format:%H', '-s', self.raw_id, '--', path] | |
296 | so = self.repository.run_git_command(cmd) |
|
295 | so = self.repository.run_git_command(cmd) | |
297 | ids = re.findall(r'[0-9a-fA-F]{40}', so) |
|
296 | ids = re.findall(r'[0-9a-fA-F]{40}', so) | |
298 | return [self.repository.get_changeset(sha) for sha in ids] |
|
297 | return [self.repository.get_changeset(sha) for sha in ids] | |
299 |
|
298 | |||
300 | def get_file_history_2(self, path): |
|
299 | def get_file_history_2(self, path): | |
301 | """ |
|
300 | """ | |
302 | Returns history of file as reversed list of ``Changeset`` objects for |
|
301 | Returns history of file as reversed list of ``Changeset`` objects for | |
303 | which file at given ``path`` has been modified. |
|
302 | which file at given ``path`` has been modified. | |
304 |
|
303 | |||
305 | """ |
|
304 | """ | |
306 | self._get_filectx(path) |
|
305 | self._get_filectx(path) | |
307 | from dulwich.walk import Walker |
|
306 | from dulwich.walk import Walker | |
308 | include = [self.raw_id] |
|
307 | include = [self.raw_id] | |
309 | walker = Walker(self.repository._repo.object_store, include, |
|
308 | walker = Walker(self.repository._repo.object_store, include, | |
310 | paths=[path], max_entries=1) |
|
309 | paths=[path], max_entries=1) | |
311 | return [self.repository.get_changeset(ascii_str(x.commit.id.decode)) |
|
310 | return [self.repository.get_changeset(ascii_str(x.commit.id.decode)) | |
312 | for x in walker] |
|
311 | for x in walker] | |
313 |
|
312 | |||
314 | def get_file_annotate(self, path): |
|
313 | def get_file_annotate(self, path): | |
315 | """ |
|
314 | """ | |
316 | Returns a generator of four element tuples with |
|
315 | Returns a generator of four element tuples with | |
317 | lineno, sha, changeset lazy loader and line |
|
316 | lineno, sha, changeset lazy loader and line | |
318 | """ |
|
317 | """ | |
319 | # TODO: This function now uses os underlying 'git' command which is |
|
318 | # TODO: This function now uses os underlying 'git' command which is | |
320 | # generally not good. Should be replaced with algorithm iterating |
|
319 | # generally not good. Should be replaced with algorithm iterating | |
321 | # commits. |
|
320 | # commits. | |
322 | cmd = ['blame', '-l', '--root', '-r', self.raw_id, '--', path] |
|
321 | cmd = ['blame', '-l', '--root', '-r', self.raw_id, '--', path] | |
323 | # -l ==> outputs long shas (and we need all 40 characters) |
|
322 | # -l ==> outputs long shas (and we need all 40 characters) | |
324 | # --root ==> doesn't put '^' character for boundaries |
|
323 | # --root ==> doesn't put '^' character for boundaries | |
325 | # -r sha ==> blames for the given revision |
|
324 | # -r sha ==> blames for the given revision | |
326 | so = self.repository.run_git_command(cmd) |
|
325 | so = self.repository.run_git_command(cmd) | |
327 |
|
326 | |||
328 | for i, blame_line in enumerate(so.split('\n')[:-1]): |
|
327 | for i, blame_line in enumerate(so.split('\n')[:-1]): | |
329 | sha, line = re.split(r' ', blame_line, 1) |
|
328 | sha, line = re.split(r' ', blame_line, 1) | |
330 | yield (i + 1, sha, lambda sha=sha: self.repository.get_changeset(sha), line) |
|
329 | yield (i + 1, sha, lambda sha=sha: self.repository.get_changeset(sha), line) | |
331 |
|
330 | |||
332 | def fill_archive(self, stream=None, kind='tgz', prefix=None, |
|
331 | def fill_archive(self, stream=None, kind='tgz', prefix=None, | |
333 | subrepos=False): |
|
332 | subrepos=False): | |
334 | """ |
|
333 | """ | |
335 | Fills up given stream. |
|
334 | Fills up given stream. | |
336 |
|
335 | |||
337 | :param stream: file like object. |
|
336 | :param stream: file like object. | |
338 | :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``. |
|
337 | :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``. | |
339 | Default: ``tgz``. |
|
338 | Default: ``tgz``. | |
340 | :param prefix: name of root directory in archive. |
|
339 | :param prefix: name of root directory in archive. | |
341 | Default is repository name and changeset's raw_id joined with dash |
|
340 | Default is repository name and changeset's raw_id joined with dash | |
342 | (``repo-tip.<KIND>``). |
|
341 | (``repo-tip.<KIND>``). | |
343 | :param subrepos: include subrepos in this archive. |
|
342 | :param subrepos: include subrepos in this archive. | |
344 |
|
343 | |||
345 | :raise ImproperArchiveTypeError: If given kind is wrong. |
|
344 | :raise ImproperArchiveTypeError: If given kind is wrong. | |
346 | :raise VcsError: If given stream is None |
|
345 | :raise VcsError: If given stream is None | |
347 | """ |
|
346 | """ | |
348 | allowed_kinds = settings.ARCHIVE_SPECS |
|
347 | allowed_kinds = settings.ARCHIVE_SPECS | |
349 | if kind not in allowed_kinds: |
|
348 | if kind not in allowed_kinds: | |
350 | raise ImproperArchiveTypeError('Archive kind not supported use one' |
|
349 | raise ImproperArchiveTypeError('Archive kind not supported use one' | |
351 | 'of %s' % ' '.join(allowed_kinds)) |
|
350 | 'of %s' % ' '.join(allowed_kinds)) | |
352 |
|
351 | |||
353 | if stream is None: |
|
352 | if stream is None: | |
354 | raise VCSError('You need to pass in a valid stream for filling' |
|
353 | raise VCSError('You need to pass in a valid stream for filling' | |
355 | ' with archival data') |
|
354 | ' with archival data') | |
356 |
|
355 | |||
357 | if prefix is None: |
|
356 | if prefix is None: | |
358 | prefix = '%s-%s' % (self.repository.name, self.short_id) |
|
357 | prefix = '%s-%s' % (self.repository.name, self.short_id) | |
359 | elif prefix.startswith('/'): |
|
358 | elif prefix.startswith('/'): | |
360 | raise VCSError("Prefix cannot start with leading slash") |
|
359 | raise VCSError("Prefix cannot start with leading slash") | |
361 | elif prefix.strip() == '': |
|
360 | elif prefix.strip() == '': | |
362 | raise VCSError("Prefix cannot be empty") |
|
361 | raise VCSError("Prefix cannot be empty") | |
363 |
|
362 | |||
364 | if kind == 'zip': |
|
363 | if kind == 'zip': | |
365 | frmt = 'zip' |
|
364 | frmt = 'zip' | |
366 | else: |
|
365 | else: | |
367 | frmt = 'tar' |
|
366 | frmt = 'tar' | |
368 | _git_path = settings.GIT_EXECUTABLE_PATH |
|
367 | _git_path = settings.GIT_EXECUTABLE_PATH | |
369 | cmd = '%s archive --format=%s --prefix=%s/ %s' % (_git_path, |
|
368 | cmd = '%s archive --format=%s --prefix=%s/ %s' % (_git_path, | |
370 | frmt, prefix, self.raw_id) |
|
369 | frmt, prefix, self.raw_id) | |
371 | if kind == 'tgz': |
|
370 | if kind == 'tgz': | |
372 | cmd += ' | gzip -9' |
|
371 | cmd += ' | gzip -9' | |
373 | elif kind == 'tbz2': |
|
372 | elif kind == 'tbz2': | |
374 | cmd += ' | bzip2 -9' |
|
373 | cmd += ' | bzip2 -9' | |
375 |
|
374 | |||
376 | if stream is None: |
|
375 | if stream is None: | |
377 | raise VCSError('You need to pass in a valid stream for filling' |
|
376 | raise VCSError('You need to pass in a valid stream for filling' | |
378 | ' with archival data') |
|
377 | ' with archival data') | |
379 | popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True, |
|
378 | popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True, | |
380 | cwd=self.repository.path) |
|
379 | cwd=self.repository.path) | |
381 |
|
380 | |||
382 | buffer_size = 1024 * 8 |
|
381 | buffer_size = 1024 * 8 | |
383 | chunk = popen.stdout.read(buffer_size) |
|
382 | chunk = popen.stdout.read(buffer_size) | |
384 | while chunk: |
|
383 | while chunk: | |
385 | stream.write(chunk) |
|
384 | stream.write(chunk) | |
386 | chunk = popen.stdout.read(buffer_size) |
|
385 | chunk = popen.stdout.read(buffer_size) | |
387 | # Make sure all descriptors would be read |
|
386 | # Make sure all descriptors would be read | |
388 | popen.communicate() |
|
387 | popen.communicate() | |
389 |
|
388 | |||
390 | def get_nodes(self, path): |
|
389 | def get_nodes(self, path): | |
391 | """ |
|
390 | """ | |
392 | Returns combined ``DirNode`` and ``FileNode`` objects list representing |
|
391 | Returns combined ``DirNode`` and ``FileNode`` objects list representing | |
393 | state of changeset at the given ``path``. If node at the given ``path`` |
|
392 | state of changeset at the given ``path``. If node at the given ``path`` | |
394 | is not instance of ``DirNode``, ChangesetError would be raised. |
|
393 | is not instance of ``DirNode``, ChangesetError would be raised. | |
395 | """ |
|
394 | """ | |
396 |
|
395 | |||
397 | if self._get_kind(path) != NodeKind.DIR: |
|
396 | if self._get_kind(path) != NodeKind.DIR: | |
398 | raise ChangesetError("Directory does not exist for revision %s at " |
|
397 | raise ChangesetError("Directory does not exist for revision %s at " | |
399 | " '%s'" % (self.revision, path)) |
|
398 | " '%s'" % (self.revision, path)) | |
400 | path = self._fix_path(path) |
|
399 | path = self._fix_path(path) | |
401 | id = self._get_id_for_path(path) |
|
400 | id = self._get_id_for_path(path) | |
402 | tree = self.repository._repo[id] |
|
401 | tree = self.repository._repo[id] | |
403 | dirnodes = [] |
|
402 | dirnodes = [] | |
404 | filenodes = [] |
|
403 | filenodes = [] | |
405 | als = self.repository.alias |
|
404 | als = self.repository.alias | |
406 | for name, stat, id in tree.items(): |
|
405 | for name, stat, id in tree.items(): | |
|
406 | obj_path = safe_str(name) | |||
407 | if path != '': |
|
407 | if path != '': | |
408 |
obj_path = '/'.join((path, |
|
408 | obj_path = '/'.join((path, obj_path)) | |
409 | else: |
|
|||
410 | obj_path = name |
|
|||
411 | if objects.S_ISGITLINK(stat): |
|
409 | if objects.S_ISGITLINK(stat): | |
412 | root_tree = self.repository._repo[self._tree_id] |
|
410 | root_tree = self.repository._repo[self._tree_id] | |
413 | cf = ConfigFile.from_file(BytesIO(self.repository._repo.get_object(root_tree[b'.gitmodules'][1]).data)) |
|
411 | cf = ConfigFile.from_file(BytesIO(self.repository._repo.get_object(root_tree[b'.gitmodules'][1]).data)) | |
414 | url = ascii_str(cf.get(('submodule', obj_path), 'url')) |
|
412 | url = ascii_str(cf.get(('submodule', obj_path), 'url')) | |
415 | dirnodes.append(SubModuleNode(obj_path, url=url, changeset=ascii_str(id), |
|
413 | dirnodes.append(SubModuleNode(obj_path, url=url, changeset=ascii_str(id), | |
416 | alias=als)) |
|
414 | alias=als)) | |
417 | continue |
|
415 | continue | |
418 |
|
416 | |||
419 | obj = self.repository._repo.get_object(id) |
|
417 | obj = self.repository._repo.get_object(id) | |
420 | if obj_path not in self._stat_modes: |
|
418 | if obj_path not in self._stat_modes: | |
421 | self._stat_modes[obj_path] = stat |
|
419 | self._stat_modes[obj_path] = stat | |
422 | if isinstance(obj, objects.Tree): |
|
420 | if isinstance(obj, objects.Tree): | |
423 | dirnodes.append(DirNode(obj_path, changeset=self)) |
|
421 | dirnodes.append(DirNode(obj_path, changeset=self)) | |
424 | elif isinstance(obj, objects.Blob): |
|
422 | elif isinstance(obj, objects.Blob): | |
425 | filenodes.append(FileNode(obj_path, changeset=self, mode=stat)) |
|
423 | filenodes.append(FileNode(obj_path, changeset=self, mode=stat)) | |
426 | else: |
|
424 | else: | |
427 | raise ChangesetError("Requested object should be Tree " |
|
425 | raise ChangesetError("Requested object should be Tree " | |
428 | "or Blob, is %r" % type(obj)) |
|
426 | "or Blob, is %r" % type(obj)) | |
429 | nodes = dirnodes + filenodes |
|
427 | nodes = dirnodes + filenodes | |
430 | for node in nodes: |
|
428 | for node in nodes: | |
431 | if node.path not in self.nodes: |
|
429 | if node.path not in self.nodes: | |
432 | self.nodes[node.path] = node |
|
430 | self.nodes[node.path] = node | |
433 | nodes.sort() |
|
431 | nodes.sort() | |
434 | return nodes |
|
432 | return nodes | |
435 |
|
433 | |||
436 | def get_node(self, path): |
|
434 | def get_node(self, path): | |
437 | """ |
|
435 | """ | |
438 | Returns ``Node`` object from the given ``path``. If there is no node at |
|
436 | Returns ``Node`` object from the given ``path``. If there is no node at | |
439 | the given ``path``, ``ChangesetError`` would be raised. |
|
437 | the given ``path``, ``ChangesetError`` would be raised. | |
440 | """ |
|
438 | """ | |
441 | path = self._fix_path(path) |
|
439 | path = self._fix_path(path) | |
442 | if path not in self.nodes: |
|
440 | if path not in self.nodes: | |
443 | try: |
|
441 | try: | |
444 | id_ = self._get_id_for_path(path) |
|
442 | id_ = self._get_id_for_path(path) | |
445 | except ChangesetError: |
|
443 | except ChangesetError: | |
446 | raise NodeDoesNotExistError("Cannot find one of parents' " |
|
444 | raise NodeDoesNotExistError("Cannot find one of parents' " | |
447 | "directories for a given path: %s" % path) |
|
445 | "directories for a given path: %s" % path) | |
448 |
|
446 | |||
449 | _GL = lambda m: m and objects.S_ISGITLINK(m) |
|
447 | _GL = lambda m: m and objects.S_ISGITLINK(m) | |
450 | if _GL(self._stat_modes.get(path)): |
|
448 | if _GL(self._stat_modes.get(path)): | |
451 | tree = self.repository._repo[self._tree_id] |
|
449 | tree = self.repository._repo[self._tree_id] | |
452 | cf = ConfigFile.from_file(BytesIO(self.repository._repo.get_object(tree[b'.gitmodules'][1]).data)) |
|
450 | cf = ConfigFile.from_file(BytesIO(self.repository._repo.get_object(tree[b'.gitmodules'][1]).data)) | |
453 | url = ascii_str(cf.get(('submodule', path), 'url')) |
|
451 | url = ascii_str(cf.get(('submodule', path), 'url')) | |
454 | node = SubModuleNode(path, url=url, changeset=ascii_str(id_), |
|
452 | node = SubModuleNode(path, url=url, changeset=ascii_str(id_), | |
455 | alias=self.repository.alias) |
|
453 | alias=self.repository.alias) | |
456 | else: |
|
454 | else: | |
457 | obj = self.repository._repo.get_object(id_) |
|
455 | obj = self.repository._repo.get_object(id_) | |
458 |
|
456 | |||
459 | if isinstance(obj, objects.Tree): |
|
457 | if isinstance(obj, objects.Tree): | |
460 | if path == '': |
|
458 | if path == '': | |
461 | node = RootNode(changeset=self) |
|
459 | node = RootNode(changeset=self) | |
462 | else: |
|
460 | else: | |
463 | node = DirNode(path, changeset=self) |
|
461 | node = DirNode(path, changeset=self) | |
464 | node._tree = obj |
|
462 | node._tree = obj | |
465 | elif isinstance(obj, objects.Blob): |
|
463 | elif isinstance(obj, objects.Blob): | |
466 | node = FileNode(path, changeset=self) |
|
464 | node = FileNode(path, changeset=self) | |
467 | node._blob = obj |
|
465 | node._blob = obj | |
468 | else: |
|
466 | else: | |
469 | raise NodeDoesNotExistError("There is no file nor directory " |
|
467 | raise NodeDoesNotExistError("There is no file nor directory " | |
470 | "at the given path: '%s' at revision %s" |
|
468 | "at the given path: '%s' at revision %s" | |
471 | % (path, self.short_id)) |
|
469 | % (path, self.short_id)) | |
472 | # cache node |
|
470 | # cache node | |
473 | self.nodes[path] = node |
|
471 | self.nodes[path] = node | |
474 | return self.nodes[path] |
|
472 | return self.nodes[path] | |
475 |
|
473 | |||
476 | @LazyProperty |
|
474 | @LazyProperty | |
477 | def affected_files(self): |
|
475 | def affected_files(self): | |
478 | """ |
|
476 | """ | |
479 | Gets a fast accessible file changes for given changeset |
|
477 | Gets a fast accessible file changes for given changeset | |
480 | """ |
|
478 | """ | |
481 | added, modified, deleted = self._changes_cache |
|
479 | added, modified, deleted = self._changes_cache | |
482 | return list(added.union(modified).union(deleted)) |
|
480 | return list(added.union(modified).union(deleted)) | |
483 |
|
481 | |||
484 | @LazyProperty |
|
482 | @LazyProperty | |
485 | def _changes_cache(self): |
|
483 | def _changes_cache(self): | |
486 | added = set() |
|
484 | added = set() | |
487 | modified = set() |
|
485 | modified = set() | |
488 | deleted = set() |
|
486 | deleted = set() | |
489 | _r = self.repository._repo |
|
487 | _r = self.repository._repo | |
490 |
|
488 | |||
491 | parents = self.parents |
|
489 | parents = self.parents | |
492 | if not self.parents: |
|
490 | if not self.parents: | |
493 | parents = [EmptyChangeset()] |
|
491 | parents = [EmptyChangeset()] | |
494 | for parent in parents: |
|
492 | for parent in parents: | |
495 | if isinstance(parent, EmptyChangeset): |
|
493 | if isinstance(parent, EmptyChangeset): | |
496 | oid = None |
|
494 | oid = None | |
497 | else: |
|
495 | else: | |
498 | oid = _r[parent._commit.id].tree |
|
496 | oid = _r[parent._commit.id].tree | |
499 | changes = _r.object_store.tree_changes(oid, _r[self._commit.id].tree) |
|
497 | changes = _r.object_store.tree_changes(oid, _r[self._commit.id].tree) | |
500 | for (oldpath, newpath), (_, _), (_, _) in changes: |
|
498 | for (oldpath, newpath), (_, _), (_, _) in changes: | |
501 | if newpath and oldpath: |
|
499 | if newpath and oldpath: | |
502 | modified.add(newpath) |
|
500 | modified.add(safe_str(newpath)) | |
503 | elif newpath and not oldpath: |
|
501 | elif newpath and not oldpath: | |
504 | added.add(newpath) |
|
502 | added.add(safe_str(newpath)) | |
505 | elif not newpath and oldpath: |
|
503 | elif not newpath and oldpath: | |
506 | deleted.add(oldpath) |
|
504 | deleted.add(safe_str(oldpath)) | |
507 | return added, modified, deleted |
|
505 | return added, modified, deleted | |
508 |
|
506 | |||
509 | def _get_paths_for_status(self, status): |
|
507 | def _get_paths_for_status(self, status): | |
510 | """ |
|
508 | """ | |
511 | Returns sorted list of paths for given ``status``. |
|
509 | Returns sorted list of paths for given ``status``. | |
512 |
|
510 | |||
513 | :param status: one of: *added*, *modified* or *deleted* |
|
511 | :param status: one of: *added*, *modified* or *deleted* | |
514 | """ |
|
512 | """ | |
515 | added, modified, deleted = self._changes_cache |
|
513 | added, modified, deleted = self._changes_cache | |
516 | return sorted({ |
|
514 | return sorted({ | |
517 | 'added': list(added), |
|
515 | 'added': list(added), | |
518 | 'modified': list(modified), |
|
516 | 'modified': list(modified), | |
519 | 'deleted': list(deleted)}[status] |
|
517 | 'deleted': list(deleted)}[status] | |
520 | ) |
|
518 | ) | |
521 |
|
519 | |||
522 | @LazyProperty |
|
520 | @LazyProperty | |
523 | def added(self): |
|
521 | def added(self): | |
524 | """ |
|
522 | """ | |
525 | Returns list of added ``FileNode`` objects. |
|
523 | Returns list of added ``FileNode`` objects. | |
526 | """ |
|
524 | """ | |
527 | if not self.parents: |
|
525 | if not self.parents: | |
528 | return list(self._get_file_nodes()) |
|
526 | return list(self._get_file_nodes()) | |
529 | return AddedFileNodesGenerator([n for n in |
|
527 | return AddedFileNodesGenerator([n for n in | |
530 | self._get_paths_for_status('added')], self) |
|
528 | self._get_paths_for_status('added')], self) | |
531 |
|
529 | |||
532 | @LazyProperty |
|
530 | @LazyProperty | |
533 | def changed(self): |
|
531 | def changed(self): | |
534 | """ |
|
532 | """ | |
535 | Returns list of modified ``FileNode`` objects. |
|
533 | Returns list of modified ``FileNode`` objects. | |
536 | """ |
|
534 | """ | |
537 | if not self.parents: |
|
535 | if not self.parents: | |
538 | return [] |
|
536 | return [] | |
539 | return ChangedFileNodesGenerator([n for n in |
|
537 | return ChangedFileNodesGenerator([n for n in | |
540 | self._get_paths_for_status('modified')], self) |
|
538 | self._get_paths_for_status('modified')], self) | |
541 |
|
539 | |||
542 | @LazyProperty |
|
540 | @LazyProperty | |
543 | def removed(self): |
|
541 | def removed(self): | |
544 | """ |
|
542 | """ | |
545 | Returns list of removed ``FileNode`` objects. |
|
543 | Returns list of removed ``FileNode`` objects. | |
546 | """ |
|
544 | """ | |
547 | if not self.parents: |
|
545 | if not self.parents: | |
548 | return [] |
|
546 | return [] | |
549 | return RemovedFileNodesGenerator([n for n in |
|
547 | return RemovedFileNodesGenerator([n for n in | |
550 | self._get_paths_for_status('deleted')], self) |
|
548 | self._get_paths_for_status('deleted')], self) | |
551 |
|
549 | |||
552 | extra = {} |
|
550 | extra = {} |
@@ -1,736 +1,737 b'' | |||||
1 | # -*- coding: utf-8 -*- |
|
1 | # -*- coding: utf-8 -*- | |
2 | """ |
|
2 | """ | |
3 | vcs.backends.git.repository |
|
3 | vcs.backends.git.repository | |
4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
5 |
|
5 | |||
6 | Git repository implementation. |
|
6 | Git repository implementation. | |
7 |
|
7 | |||
8 | :created_on: Apr 8, 2010 |
|
8 | :created_on: Apr 8, 2010 | |
9 | :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak. |
|
9 | :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak. | |
10 | """ |
|
10 | """ | |
11 |
|
11 | |||
12 | import errno |
|
12 | import errno | |
13 | import logging |
|
13 | import logging | |
14 | import os |
|
14 | import os | |
15 | import re |
|
15 | import re | |
16 | import time |
|
16 | import time | |
17 | import urllib.error |
|
17 | import urllib.error | |
18 | import urllib.parse |
|
18 | import urllib.parse | |
19 | import urllib.request |
|
19 | import urllib.request | |
20 | from collections import OrderedDict |
|
20 | from collections import OrderedDict | |
21 |
|
21 | |||
22 | import mercurial.url # import httpbasicauthhandler, httpdigestauthhandler |
|
22 | import mercurial.url # import httpbasicauthhandler, httpdigestauthhandler | |
23 | import mercurial.util # import url as hg_url |
|
23 | import mercurial.util # import url as hg_url | |
24 | from dulwich.config import ConfigFile |
|
24 | from dulwich.config import ConfigFile | |
25 | from dulwich.objects import Tag |
|
25 | from dulwich.objects import Tag | |
26 | from dulwich.repo import NotGitRepository, Repo |
|
26 | from dulwich.repo import NotGitRepository, Repo | |
27 |
|
27 | |||
28 | from kallithea.lib.vcs import subprocessio |
|
28 | from kallithea.lib.vcs import subprocessio | |
29 | from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator |
|
29 | from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator | |
30 | from kallithea.lib.vcs.conf import settings |
|
30 | from kallithea.lib.vcs.conf import settings | |
31 | from kallithea.lib.vcs.exceptions import ( |
|
31 | from kallithea.lib.vcs.exceptions import ( | |
32 | BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError, TagDoesNotExistError) |
|
32 | BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError, TagDoesNotExistError) | |
33 | from kallithea.lib.vcs.utils import ascii_str, date_fromtimestamp, makedate, safe_bytes, safe_str |
|
33 | from kallithea.lib.vcs.utils import ascii_str, date_fromtimestamp, makedate, safe_bytes, safe_str | |
34 | from kallithea.lib.vcs.utils.lazy import LazyProperty |
|
34 | from kallithea.lib.vcs.utils.lazy import LazyProperty | |
35 | from kallithea.lib.vcs.utils.paths import abspath, get_user_home |
|
35 | from kallithea.lib.vcs.utils.paths import abspath, get_user_home | |
36 |
|
36 | |||
37 | from .changeset import GitChangeset |
|
37 | from .changeset import GitChangeset | |
38 | from .inmemory import GitInMemoryChangeset |
|
38 | from .inmemory import GitInMemoryChangeset | |
39 | from .workdir import GitWorkdir |
|
39 | from .workdir import GitWorkdir | |
40 |
|
40 | |||
41 |
|
41 | |||
42 | SHA_PATTERN = re.compile(r'^([0-9a-fA-F]{12}|[0-9a-fA-F]{40})$') |
|
42 | SHA_PATTERN = re.compile(r'^([0-9a-fA-F]{12}|[0-9a-fA-F]{40})$') | |
43 |
|
43 | |||
44 | log = logging.getLogger(__name__) |
|
44 | log = logging.getLogger(__name__) | |
45 |
|
45 | |||
46 |
|
46 | |||
47 | class GitRepository(BaseRepository): |
|
47 | class GitRepository(BaseRepository): | |
48 | """ |
|
48 | """ | |
49 | Git repository backend. |
|
49 | Git repository backend. | |
50 | """ |
|
50 | """ | |
51 | DEFAULT_BRANCH_NAME = 'master' |
|
51 | DEFAULT_BRANCH_NAME = 'master' | |
52 | scm = 'git' |
|
52 | scm = 'git' | |
53 |
|
53 | |||
54 | def __init__(self, repo_path, create=False, src_url=None, |
|
54 | def __init__(self, repo_path, create=False, src_url=None, | |
55 | update_after_clone=False, bare=False): |
|
55 | update_after_clone=False, bare=False): | |
56 |
|
56 | |||
57 | self.path = abspath(repo_path) |
|
57 | self.path = abspath(repo_path) | |
58 | self.repo = self._get_repo(create, src_url, update_after_clone, bare) |
|
58 | self.repo = self._get_repo(create, src_url, update_after_clone, bare) | |
59 | self.bare = self.repo.bare |
|
59 | self.bare = self.repo.bare | |
60 |
|
60 | |||
61 | @property |
|
61 | @property | |
62 | def _config_files(self): |
|
62 | def _config_files(self): | |
63 | return [ |
|
63 | return [ | |
64 | self.bare and abspath(self.path, 'config') |
|
64 | self.bare and abspath(self.path, 'config') | |
65 | or abspath(self.path, '.git', 'config'), |
|
65 | or abspath(self.path, '.git', 'config'), | |
66 | abspath(get_user_home(), '.gitconfig'), |
|
66 | abspath(get_user_home(), '.gitconfig'), | |
67 | ] |
|
67 | ] | |
68 |
|
68 | |||
69 | @property |
|
69 | @property | |
70 | def _repo(self): |
|
70 | def _repo(self): | |
71 | return self.repo |
|
71 | return self.repo | |
72 |
|
72 | |||
73 | @property |
|
73 | @property | |
74 | def head(self): |
|
74 | def head(self): | |
75 | try: |
|
75 | try: | |
76 | return self._repo.head() |
|
76 | return self._repo.head() | |
77 | except KeyError: |
|
77 | except KeyError: | |
78 | return None |
|
78 | return None | |
79 |
|
79 | |||
80 | @property |
|
80 | @property | |
81 | def _empty(self): |
|
81 | def _empty(self): | |
82 | """ |
|
82 | """ | |
83 | Checks if repository is empty ie. without any changesets |
|
83 | Checks if repository is empty ie. without any changesets | |
84 | """ |
|
84 | """ | |
85 |
|
85 | |||
86 | try: |
|
86 | try: | |
87 | self.revisions[0] |
|
87 | self.revisions[0] | |
88 | except (KeyError, IndexError): |
|
88 | except (KeyError, IndexError): | |
89 | return True |
|
89 | return True | |
90 | return False |
|
90 | return False | |
91 |
|
91 | |||
92 | @LazyProperty |
|
92 | @LazyProperty | |
93 | def revisions(self): |
|
93 | def revisions(self): | |
94 | """ |
|
94 | """ | |
95 | Returns list of revisions' ids, in ascending order. Being lazy |
|
95 | Returns list of revisions' ids, in ascending order. Being lazy | |
96 | attribute allows external tools to inject shas from cache. |
|
96 | attribute allows external tools to inject shas from cache. | |
97 | """ |
|
97 | """ | |
98 | return self._get_all_revisions() |
|
98 | return self._get_all_revisions() | |
99 |
|
99 | |||
100 | @classmethod |
|
100 | @classmethod | |
101 | def _run_git_command(cls, cmd, cwd=None): |
|
101 | def _run_git_command(cls, cmd, cwd=None): | |
102 | """ |
|
102 | """ | |
103 | Runs given ``cmd`` as git command and returns output bytes in a tuple |
|
103 | Runs given ``cmd`` as git command and returns output bytes in a tuple | |
104 | (stdout, stderr) ... or raise RepositoryError. |
|
104 | (stdout, stderr) ... or raise RepositoryError. | |
105 |
|
105 | |||
106 | :param cmd: git command to be executed |
|
106 | :param cmd: git command to be executed | |
107 | :param cwd: passed directly to subprocess |
|
107 | :param cwd: passed directly to subprocess | |
108 | """ |
|
108 | """ | |
109 | # need to clean fix GIT_DIR ! |
|
109 | # need to clean fix GIT_DIR ! | |
110 | gitenv = dict(os.environ) |
|
110 | gitenv = dict(os.environ) | |
111 | gitenv.pop('GIT_DIR', None) |
|
111 | gitenv.pop('GIT_DIR', None) | |
112 | gitenv['GIT_CONFIG_NOGLOBAL'] = '1' |
|
112 | gitenv['GIT_CONFIG_NOGLOBAL'] = '1' | |
113 |
|
113 | |||
114 | assert isinstance(cmd, list), cmd |
|
114 | assert isinstance(cmd, list), cmd | |
115 | cmd = [settings.GIT_EXECUTABLE_PATH, '-c', 'core.quotepath=false'] + cmd |
|
115 | cmd = [settings.GIT_EXECUTABLE_PATH, '-c', 'core.quotepath=false'] + cmd | |
116 | try: |
|
116 | try: | |
117 | p = subprocessio.SubprocessIOChunker(cmd, cwd=cwd, env=gitenv, shell=False) |
|
117 | p = subprocessio.SubprocessIOChunker(cmd, cwd=cwd, env=gitenv, shell=False) | |
118 | except (EnvironmentError, OSError) as err: |
|
118 | except (EnvironmentError, OSError) as err: | |
119 | # output from the failing process is in str(EnvironmentError) |
|
119 | # output from the failing process is in str(EnvironmentError) | |
120 | msg = ("Couldn't run git command %s.\n" |
|
120 | msg = ("Couldn't run git command %s.\n" | |
121 | "Subprocess failed with '%s': %s\n" % |
|
121 | "Subprocess failed with '%s': %s\n" % | |
122 | (cmd, type(err).__name__, err) |
|
122 | (cmd, type(err).__name__, err) | |
123 | ).strip() |
|
123 | ).strip() | |
124 | log.error(msg) |
|
124 | log.error(msg) | |
125 | raise RepositoryError(msg) |
|
125 | raise RepositoryError(msg) | |
126 |
|
126 | |||
127 | try: |
|
127 | try: | |
128 | stdout = b''.join(p.output) |
|
128 | stdout = b''.join(p.output) | |
129 | stderr = b''.join(p.error) |
|
129 | stderr = b''.join(p.error) | |
130 | finally: |
|
130 | finally: | |
131 | p.close() |
|
131 | p.close() | |
132 | # TODO: introduce option to make commands fail if they have any stderr output? |
|
132 | # TODO: introduce option to make commands fail if they have any stderr output? | |
133 | if stderr: |
|
133 | if stderr: | |
134 | log.debug('stderr from %s:\n%s', cmd, stderr) |
|
134 | log.debug('stderr from %s:\n%s', cmd, stderr) | |
135 | else: |
|
135 | else: | |
136 | log.debug('stderr from %s: None', cmd) |
|
136 | log.debug('stderr from %s: None', cmd) | |
137 | return stdout, stderr |
|
137 | return stdout, stderr | |
138 |
|
138 | |||
139 | def run_git_command(self, cmd): |
|
139 | def run_git_command(self, cmd): | |
140 | """ |
|
140 | """ | |
141 | Runs given ``cmd`` as git command with cwd set to current repo. |
|
141 | Runs given ``cmd`` as git command with cwd set to current repo. | |
142 | Returns stdout as unicode str ... or raise RepositoryError. |
|
142 | Returns stdout as unicode str ... or raise RepositoryError. | |
143 | """ |
|
143 | """ | |
144 | cwd = None |
|
144 | cwd = None | |
145 | if os.path.isdir(self.path): |
|
145 | if os.path.isdir(self.path): | |
146 | cwd = self.path |
|
146 | cwd = self.path | |
147 | stdout, _stderr = self._run_git_command(cmd, cwd=cwd) |
|
147 | stdout, _stderr = self._run_git_command(cmd, cwd=cwd) | |
148 | return safe_str(stdout) |
|
148 | return safe_str(stdout) | |
149 |
|
149 | |||
150 | @classmethod |
|
150 | @classmethod | |
151 | def _check_url(cls, url): |
|
151 | def _check_url(cls, url): | |
152 | """ |
|
152 | """ | |
153 | Function will check given url and try to verify if it's a valid |
|
153 | Function will check given url and try to verify if it's a valid | |
154 | link. Sometimes it may happened that git will issue basic |
|
154 | link. Sometimes it may happened that git will issue basic | |
155 | auth request that can cause whole API to hang when used from python |
|
155 | auth request that can cause whole API to hang when used from python | |
156 | or other external calls. |
|
156 | or other external calls. | |
157 |
|
157 | |||
158 | On failures it'll raise urllib2.HTTPError, exception is also thrown |
|
158 | On failures it'll raise urllib2.HTTPError, exception is also thrown | |
159 | when the return code is non 200 |
|
159 | when the return code is non 200 | |
160 | """ |
|
160 | """ | |
161 | # check first if it's not an local url |
|
161 | # check first if it's not an local url | |
162 | if os.path.isdir(url) or url.startswith('file:'): |
|
162 | if os.path.isdir(url) or url.startswith('file:'): | |
163 | return True |
|
163 | return True | |
164 |
|
164 | |||
165 | if url.startswith('git://'): |
|
165 | if url.startswith('git://'): | |
166 | return True |
|
166 | return True | |
167 |
|
167 | |||
168 | if '+' in url[:url.find('://')]: |
|
168 | if '+' in url[:url.find('://')]: | |
169 | url = url[url.find('+') + 1:] |
|
169 | url = url[url.find('+') + 1:] | |
170 |
|
170 | |||
171 | handlers = [] |
|
171 | handlers = [] | |
172 | url_obj = mercurial.util.url(safe_bytes(url)) |
|
172 | url_obj = mercurial.util.url(safe_bytes(url)) | |
173 | test_uri, authinfo = url_obj.authinfo() |
|
173 | test_uri, authinfo = url_obj.authinfo() | |
174 | if not test_uri.endswith('info/refs'): |
|
174 | if not test_uri.endswith('info/refs'): | |
175 | test_uri = test_uri.rstrip('/') + '/info/refs' |
|
175 | test_uri = test_uri.rstrip('/') + '/info/refs' | |
176 |
|
176 | |||
177 | url_obj.passwd = b'*****' |
|
177 | url_obj.passwd = b'*****' | |
178 | cleaned_uri = str(url_obj) |
|
178 | cleaned_uri = str(url_obj) | |
179 |
|
179 | |||
180 | if authinfo: |
|
180 | if authinfo: | |
181 | # create a password manager |
|
181 | # create a password manager | |
182 | passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() |
|
182 | passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() | |
183 | passmgr.add_password(*authinfo) |
|
183 | passmgr.add_password(*authinfo) | |
184 |
|
184 | |||
185 | handlers.extend((mercurial.url.httpbasicauthhandler(passmgr), |
|
185 | handlers.extend((mercurial.url.httpbasicauthhandler(passmgr), | |
186 | mercurial.url.httpdigestauthhandler(passmgr))) |
|
186 | mercurial.url.httpdigestauthhandler(passmgr))) | |
187 |
|
187 | |||
188 | o = urllib.request.build_opener(*handlers) |
|
188 | o = urllib.request.build_opener(*handlers) | |
189 | o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git |
|
189 | o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git | |
190 |
|
190 | |||
191 | req = urllib.request.Request( |
|
191 | req = urllib.request.Request( | |
192 | "%s?%s" % ( |
|
192 | "%s?%s" % ( | |
193 | test_uri, |
|
193 | test_uri, | |
194 | urllib.parse.urlencode({"service": 'git-upload-pack'}) |
|
194 | urllib.parse.urlencode({"service": 'git-upload-pack'}) | |
195 | )) |
|
195 | )) | |
196 |
|
196 | |||
197 | try: |
|
197 | try: | |
198 | resp = o.open(req) |
|
198 | resp = o.open(req) | |
199 | if resp.code != 200: |
|
199 | if resp.code != 200: | |
200 | raise Exception('Return Code is not 200') |
|
200 | raise Exception('Return Code is not 200') | |
201 | except Exception as e: |
|
201 | except Exception as e: | |
202 | # means it cannot be cloned |
|
202 | # means it cannot be cloned | |
203 | raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e)) |
|
203 | raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e)) | |
204 |
|
204 | |||
205 | # now detect if it's proper git repo |
|
205 | # now detect if it's proper git repo | |
206 | gitdata = resp.read() |
|
206 | gitdata = resp.read() | |
207 | if 'service=git-upload-pack' not in gitdata: |
|
207 | if 'service=git-upload-pack' not in gitdata: | |
208 | raise urllib.error.URLError( |
|
208 | raise urllib.error.URLError( | |
209 | "url [%s] does not look like an git" % cleaned_uri) |
|
209 | "url [%s] does not look like an git" % cleaned_uri) | |
210 |
|
210 | |||
211 | return True |
|
211 | return True | |
212 |
|
212 | |||
213 | def _get_repo(self, create, src_url=None, update_after_clone=False, |
|
213 | def _get_repo(self, create, src_url=None, update_after_clone=False, | |
214 | bare=False): |
|
214 | bare=False): | |
215 | if create and os.path.exists(self.path): |
|
215 | if create and os.path.exists(self.path): | |
216 | raise RepositoryError("Location already exist") |
|
216 | raise RepositoryError("Location already exist") | |
217 | if src_url and not create: |
|
217 | if src_url and not create: | |
218 | raise RepositoryError("Create should be set to True if src_url is " |
|
218 | raise RepositoryError("Create should be set to True if src_url is " | |
219 | "given (clone operation creates repository)") |
|
219 | "given (clone operation creates repository)") | |
220 | try: |
|
220 | try: | |
221 | if create and src_url: |
|
221 | if create and src_url: | |
222 | GitRepository._check_url(src_url) |
|
222 | GitRepository._check_url(src_url) | |
223 | self.clone(src_url, update_after_clone, bare) |
|
223 | self.clone(src_url, update_after_clone, bare) | |
224 | return Repo(self.path) |
|
224 | return Repo(self.path) | |
225 | elif create: |
|
225 | elif create: | |
226 | os.makedirs(self.path) |
|
226 | os.makedirs(self.path) | |
227 | if bare: |
|
227 | if bare: | |
228 | return Repo.init_bare(self.path) |
|
228 | return Repo.init_bare(self.path) | |
229 | else: |
|
229 | else: | |
230 | return Repo.init(self.path) |
|
230 | return Repo.init(self.path) | |
231 | else: |
|
231 | else: | |
232 | return Repo(self.path) |
|
232 | return Repo(self.path) | |
233 | except (NotGitRepository, OSError) as err: |
|
233 | except (NotGitRepository, OSError) as err: | |
234 | raise RepositoryError(err) |
|
234 | raise RepositoryError(err) | |
235 |
|
235 | |||
236 | def _get_all_revisions(self): |
|
236 | def _get_all_revisions(self): | |
237 | # we must check if this repo is not empty, since later command |
|
237 | # we must check if this repo is not empty, since later command | |
238 | # fails if it is. And it's cheaper to ask than throw the subprocess |
|
238 | # fails if it is. And it's cheaper to ask than throw the subprocess | |
239 | # errors |
|
239 | # errors | |
240 | try: |
|
240 | try: | |
241 | self._repo.head() |
|
241 | self._repo.head() | |
242 | except KeyError: |
|
242 | except KeyError: | |
243 | return [] |
|
243 | return [] | |
244 |
|
244 | |||
245 | rev_filter = settings.GIT_REV_FILTER |
|
245 | rev_filter = settings.GIT_REV_FILTER | |
246 | cmd = ['rev-list', rev_filter, '--reverse', '--date-order'] |
|
246 | cmd = ['rev-list', rev_filter, '--reverse', '--date-order'] | |
247 | try: |
|
247 | try: | |
248 | so = self.run_git_command(cmd) |
|
248 | so = self.run_git_command(cmd) | |
249 | except RepositoryError: |
|
249 | except RepositoryError: | |
250 | # Can be raised for empty repositories |
|
250 | # Can be raised for empty repositories | |
251 | return [] |
|
251 | return [] | |
252 | return so.splitlines() |
|
252 | return so.splitlines() | |
253 |
|
253 | |||
254 | def _get_all_revisions2(self): |
|
254 | def _get_all_revisions2(self): | |
255 | # alternate implementation using dulwich |
|
255 | # alternate implementation using dulwich | |
256 | includes = [ascii_str(sha) for key, (sha, type_) in self._parsed_refs.items() |
|
256 | includes = [ascii_str(sha) for key, (sha, type_) in self._parsed_refs.items() | |
257 | if type_ != b'T'] |
|
257 | if type_ != b'T'] | |
258 | return [c.commit.id for c in self._repo.get_walker(include=includes)] |
|
258 | return [c.commit.id for c in self._repo.get_walker(include=includes)] | |
259 |
|
259 | |||
260 | def _get_revision(self, revision): |
|
260 | def _get_revision(self, revision): | |
261 | """ |
|
261 | """ | |
262 | Given any revision identifier, returns a 40 char string with revision hash. |
|
262 | Given any revision identifier, returns a 40 char string with revision hash. | |
263 | """ |
|
263 | """ | |
264 | if self._empty: |
|
264 | if self._empty: | |
265 | raise EmptyRepositoryError("There are no changesets yet") |
|
265 | raise EmptyRepositoryError("There are no changesets yet") | |
266 |
|
266 | |||
267 | if revision in (None, '', 'tip', 'HEAD', 'head', -1): |
|
267 | if revision in (None, '', 'tip', 'HEAD', 'head', -1): | |
268 | revision = -1 |
|
268 | revision = -1 | |
269 |
|
269 | |||
270 | if isinstance(revision, int): |
|
270 | if isinstance(revision, int): | |
271 | try: |
|
271 | try: | |
272 | return self.revisions[revision] |
|
272 | return self.revisions[revision] | |
273 | except IndexError: |
|
273 | except IndexError: | |
274 | msg = "Revision %r does not exist for %s" % (revision, self.name) |
|
274 | msg = "Revision %r does not exist for %s" % (revision, self.name) | |
275 | raise ChangesetDoesNotExistError(msg) |
|
275 | raise ChangesetDoesNotExistError(msg) | |
276 |
|
276 | |||
277 | if isinstance(revision, (str, unicode)): |
|
277 | if isinstance(revision, (str, unicode)): | |
278 | if revision.isdigit() and (len(revision) < 12 or len(revision) == revision.count('0')): |
|
278 | if revision.isdigit() and (len(revision) < 12 or len(revision) == revision.count('0')): | |
279 | try: |
|
279 | try: | |
280 | return self.revisions[int(revision)] |
|
280 | return self.revisions[int(revision)] | |
281 | except IndexError: |
|
281 | except IndexError: | |
282 | msg = "Revision %r does not exist for %s" % (revision, self) |
|
282 | msg = "Revision %r does not exist for %s" % (revision, self) | |
283 | raise ChangesetDoesNotExistError(msg) |
|
283 | raise ChangesetDoesNotExistError(msg) | |
284 |
|
284 | |||
285 | # get by branch/tag name |
|
285 | # get by branch/tag name | |
286 | _ref_revision = self._parsed_refs.get(safe_bytes(revision)) |
|
286 | _ref_revision = self._parsed_refs.get(safe_bytes(revision)) | |
287 | if _ref_revision: # and _ref_revision[1] in [b'H', b'RH', b'T']: |
|
287 | if _ref_revision: # and _ref_revision[1] in [b'H', b'RH', b'T']: | |
288 | return ascii_str(_ref_revision[0]) |
|
288 | return ascii_str(_ref_revision[0]) | |
289 |
|
289 | |||
290 | if revision in self.revisions: |
|
290 | if revision in self.revisions: | |
291 | return revision |
|
291 | return revision | |
292 |
|
292 | |||
293 | # maybe it's a tag ? we don't have them in self.revisions |
|
293 | # maybe it's a tag ? we don't have them in self.revisions | |
294 | if revision in self.tags.values(): |
|
294 | if revision in self.tags.values(): | |
295 | return revision |
|
295 | return revision | |
296 |
|
296 | |||
297 | if SHA_PATTERN.match(revision): |
|
297 | if SHA_PATTERN.match(revision): | |
298 | msg = "Revision %r does not exist for %s" % (revision, self.name) |
|
298 | msg = "Revision %r does not exist for %s" % (revision, self.name) | |
299 | raise ChangesetDoesNotExistError(msg) |
|
299 | raise ChangesetDoesNotExistError(msg) | |
300 |
|
300 | |||
301 | raise ChangesetDoesNotExistError("Given revision %r not recognized" % revision) |
|
301 | raise ChangesetDoesNotExistError("Given revision %r not recognized" % revision) | |
302 |
|
302 | |||
303 | def get_ref_revision(self, ref_type, ref_name): |
|
303 | def get_ref_revision(self, ref_type, ref_name): | |
304 | """ |
|
304 | """ | |
305 | Returns ``GitChangeset`` object representing repository's |
|
305 | Returns ``GitChangeset`` object representing repository's | |
306 | changeset at the given ``revision``. |
|
306 | changeset at the given ``revision``. | |
307 | """ |
|
307 | """ | |
308 | return self._get_revision(ref_name) |
|
308 | return self._get_revision(ref_name) | |
309 |
|
309 | |||
310 | def _get_archives(self, archive_name='tip'): |
|
310 | def _get_archives(self, archive_name='tip'): | |
311 |
|
311 | |||
312 | for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]: |
|
312 | for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]: | |
313 | yield {"type": i[0], "extension": i[1], "node": archive_name} |
|
313 | yield {"type": i[0], "extension": i[1], "node": archive_name} | |
314 |
|
314 | |||
315 | def _get_url(self, url): |
|
315 | def _get_url(self, url): | |
316 | """ |
|
316 | """ | |
317 | Returns normalized url. If schema is not given, would fall to |
|
317 | Returns normalized url. If schema is not given, would fall to | |
318 | filesystem (``file:///``) schema. |
|
318 | filesystem (``file:///``) schema. | |
319 | """ |
|
319 | """ | |
320 | if url != 'default' and '://' not in url: |
|
320 | if url != 'default' and '://' not in url: | |
321 | url = ':///'.join(('file', url)) |
|
321 | url = ':///'.join(('file', url)) | |
322 | return url |
|
322 | return url | |
323 |
|
323 | |||
324 | @LazyProperty |
|
324 | @LazyProperty | |
325 | def name(self): |
|
325 | def name(self): | |
326 | return os.path.basename(self.path) |
|
326 | return os.path.basename(self.path) | |
327 |
|
327 | |||
328 | @LazyProperty |
|
328 | @LazyProperty | |
329 | def last_change(self): |
|
329 | def last_change(self): | |
330 | """ |
|
330 | """ | |
331 | Returns last change made on this repository as datetime object |
|
331 | Returns last change made on this repository as datetime object | |
332 | """ |
|
332 | """ | |
333 | return date_fromtimestamp(self._get_mtime(), makedate()[1]) |
|
333 | return date_fromtimestamp(self._get_mtime(), makedate()[1]) | |
334 |
|
334 | |||
335 | def _get_mtime(self): |
|
335 | def _get_mtime(self): | |
336 | try: |
|
336 | try: | |
337 | return time.mktime(self.get_changeset().date.timetuple()) |
|
337 | return time.mktime(self.get_changeset().date.timetuple()) | |
338 | except RepositoryError: |
|
338 | except RepositoryError: | |
339 | idx_loc = '' if self.bare else '.git' |
|
339 | idx_loc = '' if self.bare else '.git' | |
340 | # fallback to filesystem |
|
340 | # fallback to filesystem | |
341 | in_path = os.path.join(self.path, idx_loc, "index") |
|
341 | in_path = os.path.join(self.path, idx_loc, "index") | |
342 | he_path = os.path.join(self.path, idx_loc, "HEAD") |
|
342 | he_path = os.path.join(self.path, idx_loc, "HEAD") | |
343 | if os.path.exists(in_path): |
|
343 | if os.path.exists(in_path): | |
344 | return os.stat(in_path).st_mtime |
|
344 | return os.stat(in_path).st_mtime | |
345 | else: |
|
345 | else: | |
346 | return os.stat(he_path).st_mtime |
|
346 | return os.stat(he_path).st_mtime | |
347 |
|
347 | |||
348 | @LazyProperty |
|
348 | @LazyProperty | |
349 | def description(self): |
|
349 | def description(self): | |
350 | return safe_str(self._repo.get_description() or b'unknown') |
|
350 | return safe_str(self._repo.get_description() or b'unknown') | |
351 |
|
351 | |||
352 | @LazyProperty |
|
352 | @LazyProperty | |
353 | def contact(self): |
|
353 | def contact(self): | |
354 | undefined_contact = u'Unknown' |
|
354 | undefined_contact = u'Unknown' | |
355 | return undefined_contact |
|
355 | return undefined_contact | |
356 |
|
356 | |||
357 | @property |
|
357 | @property | |
358 | def branches(self): |
|
358 | def branches(self): | |
359 | if not self.revisions: |
|
359 | if not self.revisions: | |
360 | return {} |
|
360 | return {} | |
361 | sortkey = lambda ctx: ctx[0] |
|
361 | sortkey = lambda ctx: ctx[0] | |
362 | _branches = [(key, ascii_str(sha)) |
|
362 | _branches = [(safe_str(key), ascii_str(sha)) | |
363 | for key, (sha, type_) in self._parsed_refs.items() if type_ == b'H'] |
|
363 | for key, (sha, type_) in self._parsed_refs.items() if type_ == b'H'] | |
364 | return OrderedDict(sorted(_branches, key=sortkey, reverse=False)) |
|
364 | return OrderedDict(sorted(_branches, key=sortkey, reverse=False)) | |
365 |
|
365 | |||
366 | @LazyProperty |
|
366 | @LazyProperty | |
367 | def closed_branches(self): |
|
367 | def closed_branches(self): | |
368 | return {} |
|
368 | return {} | |
369 |
|
369 | |||
370 | @LazyProperty |
|
370 | @LazyProperty | |
371 | def tags(self): |
|
371 | def tags(self): | |
372 | return self._get_tags() |
|
372 | return self._get_tags() | |
373 |
|
373 | |||
374 | def _get_tags(self): |
|
374 | def _get_tags(self): | |
375 | if not self.revisions: |
|
375 | if not self.revisions: | |
376 | return {} |
|
376 | return {} | |
377 |
|
377 | |||
378 | sortkey = lambda ctx: ctx[0] |
|
378 | sortkey = lambda ctx: ctx[0] | |
379 | _tags = [(key, ascii_str(sha)) |
|
379 | _tags = [(safe_str(key), ascii_str(sha)) | |
380 | for key, (sha, type_) in self._parsed_refs.items() if type_ == b'T'] |
|
380 | for key, (sha, type_) in self._parsed_refs.items() if type_ == b'T'] | |
381 | return OrderedDict(sorted(_tags, key=sortkey, reverse=True)) |
|
381 | return OrderedDict(sorted(_tags, key=sortkey, reverse=True)) | |
382 |
|
382 | |||
383 | def tag(self, name, user, revision=None, message=None, date=None, |
|
383 | def tag(self, name, user, revision=None, message=None, date=None, | |
384 | **kwargs): |
|
384 | **kwargs): | |
385 | """ |
|
385 | """ | |
386 | Creates and returns a tag for the given ``revision``. |
|
386 | Creates and returns a tag for the given ``revision``. | |
387 |
|
387 | |||
388 | :param name: name for new tag |
|
388 | :param name: name for new tag | |
389 | :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>" |
|
389 | :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>" | |
390 | :param revision: changeset id for which new tag would be created |
|
390 | :param revision: changeset id for which new tag would be created | |
391 | :param message: message of the tag's commit |
|
391 | :param message: message of the tag's commit | |
392 | :param date: date of tag's commit |
|
392 | :param date: date of tag's commit | |
393 |
|
393 | |||
394 | :raises TagAlreadyExistError: if tag with same name already exists |
|
394 | :raises TagAlreadyExistError: if tag with same name already exists | |
395 | """ |
|
395 | """ | |
396 | if name in self.tags: |
|
396 | if name in self.tags: | |
397 | raise TagAlreadyExistError("Tag %s already exists" % name) |
|
397 | raise TagAlreadyExistError("Tag %s already exists" % name) | |
398 | changeset = self.get_changeset(revision) |
|
398 | changeset = self.get_changeset(revision) | |
399 | message = message or "Added tag %s for commit %s" % (name, |
|
399 | message = message or "Added tag %s for commit %s" % (name, | |
400 | changeset.raw_id) |
|
400 | changeset.raw_id) | |
401 | self._repo.refs[b"refs/tags/%s" % safe_bytes(name)] = changeset._commit.id |
|
401 | self._repo.refs[b"refs/tags/%s" % safe_bytes(name)] = changeset._commit.id | |
402 |
|
402 | |||
403 | self._parsed_refs = self._get_parsed_refs() |
|
403 | self._parsed_refs = self._get_parsed_refs() | |
404 | self.tags = self._get_tags() |
|
404 | self.tags = self._get_tags() | |
405 | return changeset |
|
405 | return changeset | |
406 |
|
406 | |||
407 | def remove_tag(self, name, user, message=None, date=None): |
|
407 | def remove_tag(self, name, user, message=None, date=None): | |
408 | """ |
|
408 | """ | |
409 | Removes tag with the given ``name``. |
|
409 | Removes tag with the given ``name``. | |
410 |
|
410 | |||
411 | :param name: name of the tag to be removed |
|
411 | :param name: name of the tag to be removed | |
412 | :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>" |
|
412 | :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>" | |
413 | :param message: message of the tag's removal commit |
|
413 | :param message: message of the tag's removal commit | |
414 | :param date: date of tag's removal commit |
|
414 | :param date: date of tag's removal commit | |
415 |
|
415 | |||
416 | :raises TagDoesNotExistError: if tag with given name does not exists |
|
416 | :raises TagDoesNotExistError: if tag with given name does not exists | |
417 | """ |
|
417 | """ | |
418 | if name not in self.tags: |
|
418 | if name not in self.tags: | |
419 | raise TagDoesNotExistError("Tag %s does not exist" % name) |
|
419 | raise TagDoesNotExistError("Tag %s does not exist" % name) | |
420 | # self._repo.refs is a DiskRefsContainer, and .path gives the full absolute path of '.git' |
|
420 | # self._repo.refs is a DiskRefsContainer, and .path gives the full absolute path of '.git' | |
421 | tagpath = os.path.join(self._repo.refs.path, 'refs', 'tags', name) |
|
421 | tagpath = os.path.join(safe_str(self._repo.refs.path), 'refs', 'tags', name) | |
422 | try: |
|
422 | try: | |
423 | os.remove(tagpath) |
|
423 | os.remove(tagpath) | |
424 | self._parsed_refs = self._get_parsed_refs() |
|
424 | self._parsed_refs = self._get_parsed_refs() | |
425 | self.tags = self._get_tags() |
|
425 | self.tags = self._get_tags() | |
426 | except OSError as e: |
|
426 | except OSError as e: | |
427 | raise RepositoryError(e.strerror) |
|
427 | raise RepositoryError(e.strerror) | |
428 |
|
428 | |||
429 | @LazyProperty |
|
429 | @LazyProperty | |
430 | def bookmarks(self): |
|
430 | def bookmarks(self): | |
431 | """ |
|
431 | """ | |
432 | Gets bookmarks for this repository |
|
432 | Gets bookmarks for this repository | |
433 | """ |
|
433 | """ | |
434 | return {} |
|
434 | return {} | |
435 |
|
435 | |||
436 | @LazyProperty |
|
436 | @LazyProperty | |
437 | def _parsed_refs(self): |
|
437 | def _parsed_refs(self): | |
438 | return self._get_parsed_refs() |
|
438 | return self._get_parsed_refs() | |
439 |
|
439 | |||
440 | def _get_parsed_refs(self): |
|
440 | def _get_parsed_refs(self): | |
441 | """Return refs as a dict, like: |
|
441 | """Return refs as a dict, like: | |
442 | { b'v0.2.0': [b'599ba911aa24d2981225f3966eb659dfae9e9f30', b'T'] } |
|
442 | { b'v0.2.0': [b'599ba911aa24d2981225f3966eb659dfae9e9f30', b'T'] } | |
443 | """ |
|
443 | """ | |
444 | _repo = self._repo |
|
444 | _repo = self._repo | |
445 | refs = _repo.get_refs() |
|
445 | refs = _repo.get_refs() | |
446 | keys = [(b'refs/heads/', b'H'), |
|
446 | keys = [(b'refs/heads/', b'H'), | |
447 | (b'refs/remotes/origin/', b'RH'), |
|
447 | (b'refs/remotes/origin/', b'RH'), | |
448 | (b'refs/tags/', b'T')] |
|
448 | (b'refs/tags/', b'T')] | |
449 | _refs = {} |
|
449 | _refs = {} | |
450 | for ref, sha in refs.items(): |
|
450 | for ref, sha in refs.items(): | |
451 | for k, type_ in keys: |
|
451 | for k, type_ in keys: | |
452 | if ref.startswith(k): |
|
452 | if ref.startswith(k): | |
453 | _key = ref[len(k):] |
|
453 | _key = ref[len(k):] | |
454 | if type_ == b'T': |
|
454 | if type_ == b'T': | |
455 | obj = _repo.get_object(sha) |
|
455 | obj = _repo.get_object(sha) | |
456 | if isinstance(obj, Tag): |
|
456 | if isinstance(obj, Tag): | |
457 | sha = _repo.get_object(sha).object[1] |
|
457 | sha = _repo.get_object(sha).object[1] | |
458 | _refs[_key] = [sha, type_] |
|
458 | _refs[_key] = [sha, type_] | |
459 | break |
|
459 | break | |
460 | return _refs |
|
460 | return _refs | |
461 |
|
461 | |||
462 | def _heads(self, reverse=False): |
|
462 | def _heads(self, reverse=False): | |
463 | refs = self._repo.get_refs() |
|
463 | refs = self._repo.get_refs() | |
464 | heads = {} |
|
464 | heads = {} | |
465 |
|
465 | |||
466 | for key, val in refs.items(): |
|
466 | for key, val in refs.items(): | |
467 | for ref_key in [b'refs/heads/', b'refs/remotes/origin/']: |
|
467 | for ref_key in [b'refs/heads/', b'refs/remotes/origin/']: | |
468 | if key.startswith(ref_key): |
|
468 | if key.startswith(ref_key): | |
469 | n = key[len(ref_key):] |
|
469 | n = key[len(ref_key):] | |
470 | if n not in [b'HEAD']: |
|
470 | if n not in [b'HEAD']: | |
471 | heads[n] = val |
|
471 | heads[n] = val | |
472 |
|
472 | |||
473 | return heads if reverse else dict((y, x) for x, y in heads.items()) |
|
473 | return heads if reverse else dict((y, x) for x, y in heads.items()) | |
474 |
|
474 | |||
475 | def get_changeset(self, revision=None): |
|
475 | def get_changeset(self, revision=None): | |
476 | """ |
|
476 | """ | |
477 | Returns ``GitChangeset`` object representing commit from git repository |
|
477 | Returns ``GitChangeset`` object representing commit from git repository | |
478 | at the given revision or head (most recent commit) if None given. |
|
478 | at the given revision or head (most recent commit) if None given. | |
479 | """ |
|
479 | """ | |
480 | if isinstance(revision, GitChangeset): |
|
480 | if isinstance(revision, GitChangeset): | |
481 | return revision |
|
481 | return revision | |
482 | return GitChangeset(repository=self, revision=self._get_revision(revision)) |
|
482 | return GitChangeset(repository=self, revision=self._get_revision(revision)) | |
483 |
|
483 | |||
484 | def get_changesets(self, start=None, end=None, start_date=None, |
|
484 | def get_changesets(self, start=None, end=None, start_date=None, | |
485 | end_date=None, branch_name=None, reverse=False, max_revisions=None): |
|
485 | end_date=None, branch_name=None, reverse=False, max_revisions=None): | |
486 | """ |
|
486 | """ | |
487 | Returns iterator of ``GitChangeset`` objects from start to end (both |
|
487 | Returns iterator of ``GitChangeset`` objects from start to end (both | |
488 | are inclusive), in ascending date order (unless ``reverse`` is set). |
|
488 | are inclusive), in ascending date order (unless ``reverse`` is set). | |
489 |
|
489 | |||
490 | :param start: changeset ID, as str; first returned changeset |
|
490 | :param start: changeset ID, as str; first returned changeset | |
491 | :param end: changeset ID, as str; last returned changeset |
|
491 | :param end: changeset ID, as str; last returned changeset | |
492 | :param start_date: if specified, changesets with commit date less than |
|
492 | :param start_date: if specified, changesets with commit date less than | |
493 | ``start_date`` would be filtered out from returned set |
|
493 | ``start_date`` would be filtered out from returned set | |
494 | :param end_date: if specified, changesets with commit date greater than |
|
494 | :param end_date: if specified, changesets with commit date greater than | |
495 | ``end_date`` would be filtered out from returned set |
|
495 | ``end_date`` would be filtered out from returned set | |
496 | :param branch_name: if specified, changesets not reachable from given |
|
496 | :param branch_name: if specified, changesets not reachable from given | |
497 | branch would be filtered out from returned set |
|
497 | branch would be filtered out from returned set | |
498 | :param reverse: if ``True``, returned generator would be reversed |
|
498 | :param reverse: if ``True``, returned generator would be reversed | |
499 | (meaning that returned changesets would have descending date order) |
|
499 | (meaning that returned changesets would have descending date order) | |
500 |
|
500 | |||
501 | :raise BranchDoesNotExistError: If given ``branch_name`` does not |
|
501 | :raise BranchDoesNotExistError: If given ``branch_name`` does not | |
502 | exist. |
|
502 | exist. | |
503 | :raise ChangesetDoesNotExistError: If changeset for given ``start`` or |
|
503 | :raise ChangesetDoesNotExistError: If changeset for given ``start`` or | |
504 | ``end`` could not be found. |
|
504 | ``end`` could not be found. | |
505 |
|
505 | |||
506 | """ |
|
506 | """ | |
507 | if branch_name and branch_name not in self.branches: |
|
507 | if branch_name and branch_name not in self.branches: | |
508 | raise BranchDoesNotExistError("Branch '%s' not found" |
|
508 | raise BranchDoesNotExistError("Branch '%s' not found" | |
509 | % branch_name) |
|
509 | % branch_name) | |
510 | # actually we should check now if it's not an empty repo to not spaw |
|
510 | # actually we should check now if it's not an empty repo to not spaw | |
511 | # subprocess commands |
|
511 | # subprocess commands | |
512 | if self._empty: |
|
512 | if self._empty: | |
513 | raise EmptyRepositoryError("There are no changesets yet") |
|
513 | raise EmptyRepositoryError("There are no changesets yet") | |
514 |
|
514 | |||
515 | # %H at format means (full) commit hash, initial hashes are retrieved |
|
515 | # %H at format means (full) commit hash, initial hashes are retrieved | |
516 | # in ascending date order |
|
516 | # in ascending date order | |
517 | cmd = ['log', '--date-order', '--reverse', '--pretty=format:%H'] |
|
517 | cmd = ['log', '--date-order', '--reverse', '--pretty=format:%H'] | |
518 | if max_revisions: |
|
518 | if max_revisions: | |
519 | cmd += ['--max-count=%s' % max_revisions] |
|
519 | cmd += ['--max-count=%s' % max_revisions] | |
520 | if start_date: |
|
520 | if start_date: | |
521 | cmd += ['--since', start_date.strftime('%m/%d/%y %H:%M:%S')] |
|
521 | cmd += ['--since', start_date.strftime('%m/%d/%y %H:%M:%S')] | |
522 | if end_date: |
|
522 | if end_date: | |
523 | cmd += ['--until', end_date.strftime('%m/%d/%y %H:%M:%S')] |
|
523 | cmd += ['--until', end_date.strftime('%m/%d/%y %H:%M:%S')] | |
524 | if branch_name: |
|
524 | if branch_name: | |
525 | cmd.append(branch_name) |
|
525 | cmd.append(branch_name) | |
526 | else: |
|
526 | else: | |
527 | cmd.append(settings.GIT_REV_FILTER) |
|
527 | cmd.append(settings.GIT_REV_FILTER) | |
528 |
|
528 | |||
529 | revs = self.run_git_command(cmd).splitlines() |
|
529 | revs = self.run_git_command(cmd).splitlines() | |
530 | start_pos = 0 |
|
530 | start_pos = 0 | |
531 | end_pos = len(revs) |
|
531 | end_pos = len(revs) | |
532 | if start: |
|
532 | if start: | |
533 | _start = self._get_revision(start) |
|
533 | _start = self._get_revision(start) | |
534 | try: |
|
534 | try: | |
535 | start_pos = revs.index(_start) |
|
535 | start_pos = revs.index(_start) | |
536 | except ValueError: |
|
536 | except ValueError: | |
537 | pass |
|
537 | pass | |
538 |
|
538 | |||
539 | if end is not None: |
|
539 | if end is not None: | |
540 | _end = self._get_revision(end) |
|
540 | _end = self._get_revision(end) | |
541 | try: |
|
541 | try: | |
542 | end_pos = revs.index(_end) |
|
542 | end_pos = revs.index(_end) | |
543 | except ValueError: |
|
543 | except ValueError: | |
544 | pass |
|
544 | pass | |
545 |
|
545 | |||
546 | if None not in [start, end] and start_pos > end_pos: |
|
546 | if None not in [start, end] and start_pos > end_pos: | |
547 | raise RepositoryError('start cannot be after end') |
|
547 | raise RepositoryError('start cannot be after end') | |
548 |
|
548 | |||
549 | if end_pos is not None: |
|
549 | if end_pos is not None: | |
550 | end_pos += 1 |
|
550 | end_pos += 1 | |
551 |
|
551 | |||
552 | revs = revs[start_pos:end_pos] |
|
552 | revs = revs[start_pos:end_pos] | |
553 | if reverse: |
|
553 | if reverse: | |
554 | revs.reverse() |
|
554 | revs.reverse() | |
555 |
|
555 | |||
556 | return CollectionGenerator(self, revs) |
|
556 | return CollectionGenerator(self, revs) | |
557 |
|
557 | |||
558 | def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False, |
|
558 | def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False, | |
559 | context=3): |
|
559 | context=3): | |
560 | """ |
|
560 | """ | |
561 | Returns (git like) *diff*, as plain bytes text. Shows changes |
|
561 | Returns (git like) *diff*, as plain bytes text. Shows changes | |
562 | introduced by ``rev2`` since ``rev1``. |
|
562 | introduced by ``rev2`` since ``rev1``. | |
563 |
|
563 | |||
564 | :param rev1: Entry point from which diff is shown. Can be |
|
564 | :param rev1: Entry point from which diff is shown. Can be | |
565 | ``self.EMPTY_CHANGESET`` - in this case, patch showing all |
|
565 | ``self.EMPTY_CHANGESET`` - in this case, patch showing all | |
566 | the changes since empty state of the repository until ``rev2`` |
|
566 | the changes since empty state of the repository until ``rev2`` | |
567 | :param rev2: Until which revision changes should be shown. |
|
567 | :param rev2: Until which revision changes should be shown. | |
568 | :param ignore_whitespace: If set to ``True``, would not show whitespace |
|
568 | :param ignore_whitespace: If set to ``True``, would not show whitespace | |
569 | changes. Defaults to ``False``. |
|
569 | changes. Defaults to ``False``. | |
570 | :param context: How many lines before/after changed lines should be |
|
570 | :param context: How many lines before/after changed lines should be | |
571 | shown. Defaults to ``3``. Due to limitations in Git, if |
|
571 | shown. Defaults to ``3``. Due to limitations in Git, if | |
572 | value passed-in is greater than ``2**31-1`` |
|
572 | value passed-in is greater than ``2**31-1`` | |
573 | (``2147483647``), it will be set to ``2147483647`` |
|
573 | (``2147483647``), it will be set to ``2147483647`` | |
574 | instead. If negative value is passed-in, it will be set to |
|
574 | instead. If negative value is passed-in, it will be set to | |
575 | ``0`` instead. |
|
575 | ``0`` instead. | |
576 | """ |
|
576 | """ | |
577 |
|
577 | |||
578 | # Git internally uses a signed long int for storing context |
|
578 | # Git internally uses a signed long int for storing context | |
579 | # size (number of lines to show before and after the |
|
579 | # size (number of lines to show before and after the | |
580 | # differences). This can result in integer overflow, so we |
|
580 | # differences). This can result in integer overflow, so we | |
581 | # ensure the requested context is smaller by one than the |
|
581 | # ensure the requested context is smaller by one than the | |
582 | # number that would cause the overflow. It is highly unlikely |
|
582 | # number that would cause the overflow. It is highly unlikely | |
583 | # that a single file will contain that many lines, so this |
|
583 | # that a single file will contain that many lines, so this | |
584 | # kind of change should not cause any realistic consequences. |
|
584 | # kind of change should not cause any realistic consequences. | |
585 | overflowed_long_int = 2**31 |
|
585 | overflowed_long_int = 2**31 | |
586 |
|
586 | |||
587 | if context >= overflowed_long_int: |
|
587 | if context >= overflowed_long_int: | |
588 | context = overflowed_long_int - 1 |
|
588 | context = overflowed_long_int - 1 | |
589 |
|
589 | |||
590 | # Negative context values make no sense, and will result in |
|
590 | # Negative context values make no sense, and will result in | |
591 | # errors. Ensure this does not happen. |
|
591 | # errors. Ensure this does not happen. | |
592 | if context < 0: |
|
592 | if context < 0: | |
593 | context = 0 |
|
593 | context = 0 | |
594 |
|
594 | |||
595 | flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40'] |
|
595 | flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40'] | |
596 | if ignore_whitespace: |
|
596 | if ignore_whitespace: | |
597 | flags.append('-w') |
|
597 | flags.append('-w') | |
598 |
|
598 | |||
599 | if hasattr(rev1, 'raw_id'): |
|
599 | if hasattr(rev1, 'raw_id'): | |
600 | rev1 = getattr(rev1, 'raw_id') |
|
600 | rev1 = getattr(rev1, 'raw_id') | |
601 |
|
601 | |||
602 | if hasattr(rev2, 'raw_id'): |
|
602 | if hasattr(rev2, 'raw_id'): | |
603 | rev2 = getattr(rev2, 'raw_id') |
|
603 | rev2 = getattr(rev2, 'raw_id') | |
604 |
|
604 | |||
605 | if rev1 == self.EMPTY_CHANGESET: |
|
605 | if rev1 == self.EMPTY_CHANGESET: | |
606 | rev2 = self.get_changeset(rev2).raw_id |
|
606 | rev2 = self.get_changeset(rev2).raw_id | |
607 | cmd = ['show'] + flags + [rev2] |
|
607 | cmd = ['show'] + flags + [rev2] | |
608 | else: |
|
608 | else: | |
609 | rev1 = self.get_changeset(rev1).raw_id |
|
609 | rev1 = self.get_changeset(rev1).raw_id | |
610 | rev2 = self.get_changeset(rev2).raw_id |
|
610 | rev2 = self.get_changeset(rev2).raw_id | |
611 | cmd = ['diff'] + flags + [rev1, rev2] |
|
611 | cmd = ['diff'] + flags + [rev1, rev2] | |
612 |
|
612 | |||
613 | if path: |
|
613 | if path: | |
614 | cmd += ['--', path] |
|
614 | cmd += ['--', path] | |
615 |
|
615 | |||
616 | stdout, stderr = self._run_git_command(cmd, cwd=self.path) |
|
616 | stdout, stderr = self._run_git_command(cmd, cwd=self.path) | |
617 | # If we used 'show' command, strip first few lines (until actual diff |
|
617 | # If we used 'show' command, strip first few lines (until actual diff | |
618 | # starts) |
|
618 | # starts) | |
619 | if rev1 == self.EMPTY_CHANGESET: |
|
619 | if rev1 == self.EMPTY_CHANGESET: | |
620 | parts = stdout.split(b'\ndiff ', 1) |
|
620 | parts = stdout.split(b'\ndiff ', 1) | |
621 | if len(parts) > 1: |
|
621 | if len(parts) > 1: | |
622 | stdout = b'diff ' + parts[1] |
|
622 | stdout = b'diff ' + parts[1] | |
623 | return stdout |
|
623 | return stdout | |
624 |
|
624 | |||
625 | @LazyProperty |
|
625 | @LazyProperty | |
626 | def in_memory_changeset(self): |
|
626 | def in_memory_changeset(self): | |
627 | """ |
|
627 | """ | |
628 | Returns ``GitInMemoryChangeset`` object for this repository. |
|
628 | Returns ``GitInMemoryChangeset`` object for this repository. | |
629 | """ |
|
629 | """ | |
630 | return GitInMemoryChangeset(self) |
|
630 | return GitInMemoryChangeset(self) | |
631 |
|
631 | |||
632 | def clone(self, url, update_after_clone=True, bare=False): |
|
632 | def clone(self, url, update_after_clone=True, bare=False): | |
633 | """ |
|
633 | """ | |
634 | Tries to clone changes from external location. |
|
634 | Tries to clone changes from external location. | |
635 |
|
635 | |||
636 | :param update_after_clone: If set to ``False``, git won't checkout |
|
636 | :param update_after_clone: If set to ``False``, git won't checkout | |
637 | working directory |
|
637 | working directory | |
638 | :param bare: If set to ``True``, repository would be cloned into |
|
638 | :param bare: If set to ``True``, repository would be cloned into | |
639 | *bare* git repository (no working directory at all). |
|
639 | *bare* git repository (no working directory at all). | |
640 | """ |
|
640 | """ | |
641 | url = self._get_url(url) |
|
641 | url = self._get_url(url) | |
642 | cmd = ['clone', '-q'] |
|
642 | cmd = ['clone', '-q'] | |
643 | if bare: |
|
643 | if bare: | |
644 | cmd.append('--bare') |
|
644 | cmd.append('--bare') | |
645 | elif not update_after_clone: |
|
645 | elif not update_after_clone: | |
646 | cmd.append('--no-checkout') |
|
646 | cmd.append('--no-checkout') | |
647 | cmd += ['--', url, self.path] |
|
647 | cmd += ['--', url, self.path] | |
648 | # If error occurs run_git_command raises RepositoryError already |
|
648 | # If error occurs run_git_command raises RepositoryError already | |
649 | self.run_git_command(cmd) |
|
649 | self.run_git_command(cmd) | |
650 |
|
650 | |||
651 | def pull(self, url): |
|
651 | def pull(self, url): | |
652 | """ |
|
652 | """ | |
653 | Tries to pull changes from external location. |
|
653 | Tries to pull changes from external location. | |
654 | """ |
|
654 | """ | |
655 | url = self._get_url(url) |
|
655 | url = self._get_url(url) | |
656 | cmd = ['pull', '--ff-only', url] |
|
656 | cmd = ['pull', '--ff-only', url] | |
657 | # If error occurs run_git_command raises RepositoryError already |
|
657 | # If error occurs run_git_command raises RepositoryError already | |
658 | self.run_git_command(cmd) |
|
658 | self.run_git_command(cmd) | |
659 |
|
659 | |||
660 | def fetch(self, url): |
|
660 | def fetch(self, url): | |
661 | """ |
|
661 | """ | |
662 | Tries to pull changes from external location. |
|
662 | Tries to pull changes from external location. | |
663 | """ |
|
663 | """ | |
664 | url = self._get_url(url) |
|
664 | url = self._get_url(url) | |
665 | so = self.run_git_command(['ls-remote', '-h', url]) |
|
665 | so = self.run_git_command(['ls-remote', '-h', url]) | |
666 | cmd = ['fetch', url, '--'] |
|
666 | cmd = ['fetch', url, '--'] | |
667 | for line in (x for x in so.splitlines()): |
|
667 | for line in (x for x in so.splitlines()): | |
668 | sha, ref = line.split('\t') |
|
668 | sha, ref = line.split('\t') | |
669 | cmd.append('+%s:%s' % (ref, ref)) |
|
669 | cmd.append('+%s:%s' % (ref, ref)) | |
670 | self.run_git_command(cmd) |
|
670 | self.run_git_command(cmd) | |
671 |
|
671 | |||
672 | def _update_server_info(self): |
|
672 | def _update_server_info(self): | |
673 | """ |
|
673 | """ | |
674 | runs gits update-server-info command in this repo instance |
|
674 | runs gits update-server-info command in this repo instance | |
675 | """ |
|
675 | """ | |
676 | from dulwich.server import update_server_info |
|
676 | from dulwich.server import update_server_info | |
677 | try: |
|
677 | try: | |
678 | update_server_info(self._repo) |
|
678 | update_server_info(self._repo) | |
679 | except OSError as e: |
|
679 | except OSError as e: | |
680 | if e.errno not in [errno.ENOENT, errno.EROFS]: |
|
680 | if e.errno not in [errno.ENOENT, errno.EROFS]: | |
681 | raise |
|
681 | raise | |
682 | # Workaround for dulwich crashing on for example its own dulwich/tests/data/repos/simple_merge.git/info/refs.lock |
|
682 | # Workaround for dulwich crashing on for example its own dulwich/tests/data/repos/simple_merge.git/info/refs.lock | |
683 | log.error('Ignoring %s running update-server-info: %s', type(e).__name__, e) |
|
683 | log.error('Ignoring %s running update-server-info: %s', type(e).__name__, e) | |
684 |
|
684 | |||
685 | @LazyProperty |
|
685 | @LazyProperty | |
686 | def workdir(self): |
|
686 | def workdir(self): | |
687 | """ |
|
687 | """ | |
688 | Returns ``Workdir`` instance for this repository. |
|
688 | Returns ``Workdir`` instance for this repository. | |
689 | """ |
|
689 | """ | |
690 | return GitWorkdir(self) |
|
690 | return GitWorkdir(self) | |
691 |
|
691 | |||
692 | def get_config_value(self, section, name, config_file=None): |
|
692 | def get_config_value(self, section, name, config_file=None): | |
693 | """ |
|
693 | """ | |
694 | Returns configuration value for a given [``section``] and ``name``. |
|
694 | Returns configuration value for a given [``section``] and ``name``. | |
695 |
|
695 | |||
696 | :param section: Section we want to retrieve value from |
|
696 | :param section: Section we want to retrieve value from | |
697 | :param name: Name of configuration we want to retrieve |
|
697 | :param name: Name of configuration we want to retrieve | |
698 | :param config_file: A path to file which should be used to retrieve |
|
698 | :param config_file: A path to file which should be used to retrieve | |
699 | configuration from (might also be a list of file paths) |
|
699 | configuration from (might also be a list of file paths) | |
700 | """ |
|
700 | """ | |
701 | if config_file is None: |
|
701 | if config_file is None: | |
702 | config_file = [] |
|
702 | config_file = [] | |
703 | elif isinstance(config_file, str): |
|
703 | elif isinstance(config_file, str): | |
704 | config_file = [config_file] |
|
704 | config_file = [config_file] | |
705 |
|
705 | |||
706 | def gen_configs(): |
|
706 | def gen_configs(): | |
707 | for path in config_file + self._config_files: |
|
707 | for path in config_file + self._config_files: | |
708 | try: |
|
708 | try: | |
709 | yield ConfigFile.from_path(path) |
|
709 | yield ConfigFile.from_path(path) | |
710 | except (IOError, OSError, ValueError): |
|
710 | except (IOError, OSError, ValueError): | |
711 | continue |
|
711 | continue | |
712 |
|
712 | |||
713 | for config in gen_configs(): |
|
713 | for config in gen_configs(): | |
714 | try: |
|
714 | try: | |
715 |
|
|
715 | value = config.get(section, name) | |
716 | except KeyError: |
|
716 | except KeyError: | |
717 | continue |
|
717 | continue | |
|
718 | return None if value is None else safe_str(value) | |||
718 | return None |
|
719 | return None | |
719 |
|
720 | |||
720 | def get_user_name(self, config_file=None): |
|
721 | def get_user_name(self, config_file=None): | |
721 | """ |
|
722 | """ | |
722 | Returns user's name from global configuration file. |
|
723 | Returns user's name from global configuration file. | |
723 |
|
724 | |||
724 | :param config_file: A path to file which should be used to retrieve |
|
725 | :param config_file: A path to file which should be used to retrieve | |
725 | configuration from (might also be a list of file paths) |
|
726 | configuration from (might also be a list of file paths) | |
726 | """ |
|
727 | """ | |
727 | return self.get_config_value('user', 'name', config_file) |
|
728 | return self.get_config_value('user', 'name', config_file) | |
728 |
|
729 | |||
729 | def get_user_email(self, config_file=None): |
|
730 | def get_user_email(self, config_file=None): | |
730 | """ |
|
731 | """ | |
731 | Returns user's email from global configuration file. |
|
732 | Returns user's email from global configuration file. | |
732 |
|
733 | |||
733 | :param config_file: A path to file which should be used to retrieve |
|
734 | :param config_file: A path to file which should be used to retrieve | |
734 | configuration from (might also be a list of file paths) |
|
735 | configuration from (might also be a list of file paths) | |
735 | """ |
|
736 | """ | |
736 | return self.get_config_value('user', 'email', config_file) |
|
737 | return self.get_config_value('user', 'email', config_file) |
@@ -1,32 +1,32 b'' | |||||
1 | import re |
|
1 | import re | |
2 |
|
2 | |||
3 | from kallithea.lib.utils2 import ascii_str |
|
3 | from kallithea.lib.utils2 import ascii_str, safe_str | |
4 | from kallithea.lib.vcs.backends.base import BaseWorkdir |
|
4 | from kallithea.lib.vcs.backends.base import BaseWorkdir | |
5 | from kallithea.lib.vcs.exceptions import BranchDoesNotExistError, RepositoryError |
|
5 | from kallithea.lib.vcs.exceptions import BranchDoesNotExistError, RepositoryError | |
6 |
|
6 | |||
7 |
|
7 | |||
8 | class GitWorkdir(BaseWorkdir): |
|
8 | class GitWorkdir(BaseWorkdir): | |
9 |
|
9 | |||
10 | def get_branch(self): |
|
10 | def get_branch(self): | |
11 | headpath = self.repository._repo.refs.refpath(b'HEAD') |
|
11 | headpath = self.repository._repo.refs.refpath(b'HEAD') | |
12 | try: |
|
12 | try: | |
13 | content = open(headpath).read() |
|
13 | content = safe_str(open(headpath, 'rb').read()) | |
14 | match = re.match(r'^ref: refs/heads/(?P<branch>.+)\n$', content) |
|
14 | match = re.match(r'^ref: refs/heads/(?P<branch>.+)\n$', content) | |
15 | if match: |
|
15 | if match: | |
16 | return match.groupdict()['branch'] |
|
16 | return match.groupdict()['branch'] | |
17 | else: |
|
17 | else: | |
18 | raise RepositoryError("Couldn't compute workdir's branch") |
|
18 | raise RepositoryError("Couldn't compute workdir's branch") | |
19 | except IOError: |
|
19 | except IOError: | |
20 | # Try naive way... |
|
20 | # Try naive way... | |
21 | raise RepositoryError("Couldn't compute workdir's branch") |
|
21 | raise RepositoryError("Couldn't compute workdir's branch") | |
22 |
|
22 | |||
23 | def get_changeset(self): |
|
23 | def get_changeset(self): | |
24 | wk_dir_id = ascii_str(self.repository._repo.refs.as_dict().get(b'HEAD')) |
|
24 | wk_dir_id = ascii_str(self.repository._repo.refs.as_dict().get(b'HEAD')) | |
25 | return self.repository.get_changeset(wk_dir_id) |
|
25 | return self.repository.get_changeset(wk_dir_id) | |
26 |
|
26 | |||
27 | def checkout_branch(self, branch=None): |
|
27 | def checkout_branch(self, branch=None): | |
28 | if branch is None: |
|
28 | if branch is None: | |
29 | branch = self.repository.DEFAULT_BRANCH_NAME |
|
29 | branch = self.repository.DEFAULT_BRANCH_NAME | |
30 | if branch not in self.repository.branches: |
|
30 | if branch not in self.repository.branches: | |
31 | raise BranchDoesNotExistError |
|
31 | raise BranchDoesNotExistError | |
32 | self.repository.run_git_command(['checkout', branch]) |
|
32 | self.repository.run_git_command(['checkout', branch]) |
@@ -1,407 +1,407 b'' | |||||
1 | import os |
|
1 | import os | |
2 | import posixpath |
|
2 | import posixpath | |
3 |
|
3 | |||
4 | import mercurial.archival |
|
4 | import mercurial.archival | |
5 | import mercurial.node |
|
5 | import mercurial.node | |
6 | import mercurial.obsutil |
|
6 | import mercurial.obsutil | |
7 |
|
7 | |||
8 | from kallithea.lib.vcs.backends.base import BaseChangeset |
|
8 | from kallithea.lib.vcs.backends.base import BaseChangeset | |
9 | from kallithea.lib.vcs.conf import settings |
|
9 | from kallithea.lib.vcs.conf import settings | |
10 | from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError |
|
10 | from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError | |
11 | from kallithea.lib.vcs.nodes import ( |
|
11 | from kallithea.lib.vcs.nodes import ( | |
12 | AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode) |
|
12 | AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode) | |
13 | from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, date_fromtimestamp, safe_bytes, safe_str |
|
13 | from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, date_fromtimestamp, safe_bytes, safe_str | |
14 | from kallithea.lib.vcs.utils.lazy import LazyProperty |
|
14 | from kallithea.lib.vcs.utils.lazy import LazyProperty | |
15 | from kallithea.lib.vcs.utils.paths import get_dirs_for_path |
|
15 | from kallithea.lib.vcs.utils.paths import get_dirs_for_path | |
16 |
|
16 | |||
17 |
|
17 | |||
18 | class MercurialChangeset(BaseChangeset): |
|
18 | class MercurialChangeset(BaseChangeset): | |
19 | """ |
|
19 | """ | |
20 | Represents state of the repository at a revision. |
|
20 | Represents state of the repository at a revision. | |
21 | """ |
|
21 | """ | |
22 |
|
22 | |||
23 | def __init__(self, repository, revision): |
|
23 | def __init__(self, repository, revision): | |
24 | self.repository = repository |
|
24 | self.repository = repository | |
25 | assert isinstance(revision, str), repr(revision) |
|
25 | assert isinstance(revision, str), repr(revision) | |
26 | self._ctx = repository._repo[ascii_bytes(revision)] |
|
26 | self._ctx = repository._repo[ascii_bytes(revision)] | |
27 | self.raw_id = ascii_str(self._ctx.hex()) |
|
27 | self.raw_id = ascii_str(self._ctx.hex()) | |
28 | self.revision = self._ctx._rev |
|
28 | self.revision = self._ctx._rev | |
29 | self.nodes = {} |
|
29 | self.nodes = {} | |
30 |
|
30 | |||
31 | @LazyProperty |
|
31 | @LazyProperty | |
32 | def tags(self): |
|
32 | def tags(self): | |
33 | return [safe_str(tag) for tag in self._ctx.tags()] |
|
33 | return [safe_str(tag) for tag in self._ctx.tags()] | |
34 |
|
34 | |||
35 | @LazyProperty |
|
35 | @LazyProperty | |
36 | def branch(self): |
|
36 | def branch(self): | |
37 | return safe_str(self._ctx.branch()) |
|
37 | return safe_str(self._ctx.branch()) | |
38 |
|
38 | |||
39 | @LazyProperty |
|
39 | @LazyProperty | |
40 | def branches(self): |
|
40 | def branches(self): | |
41 | return [safe_str(self._ctx.branch())] |
|
41 | return [safe_str(self._ctx.branch())] | |
42 |
|
42 | |||
43 | @LazyProperty |
|
43 | @LazyProperty | |
44 | def closesbranch(self): |
|
44 | def closesbranch(self): | |
45 | return self._ctx.closesbranch() |
|
45 | return self._ctx.closesbranch() | |
46 |
|
46 | |||
47 | @LazyProperty |
|
47 | @LazyProperty | |
48 | def obsolete(self): |
|
48 | def obsolete(self): | |
49 | return self._ctx.obsolete() |
|
49 | return self._ctx.obsolete() | |
50 |
|
50 | |||
51 | @LazyProperty |
|
51 | @LazyProperty | |
52 | def bumped(self): |
|
52 | def bumped(self): | |
53 | return self._ctx.phasedivergent() |
|
53 | return self._ctx.phasedivergent() | |
54 |
|
54 | |||
55 | @LazyProperty |
|
55 | @LazyProperty | |
56 | def divergent(self): |
|
56 | def divergent(self): | |
57 | return self._ctx.contentdivergent() |
|
57 | return self._ctx.contentdivergent() | |
58 |
|
58 | |||
59 | @LazyProperty |
|
59 | @LazyProperty | |
60 | def extinct(self): |
|
60 | def extinct(self): | |
61 | return self._ctx.extinct() |
|
61 | return self._ctx.extinct() | |
62 |
|
62 | |||
63 | @LazyProperty |
|
63 | @LazyProperty | |
64 | def unstable(self): |
|
64 | def unstable(self): | |
65 | return self._ctx.orphan() |
|
65 | return self._ctx.orphan() | |
66 |
|
66 | |||
67 | @LazyProperty |
|
67 | @LazyProperty | |
68 | def phase(self): |
|
68 | def phase(self): | |
69 | if(self._ctx.phase() == 1): |
|
69 | if(self._ctx.phase() == 1): | |
70 | return 'Draft' |
|
70 | return 'Draft' | |
71 | elif(self._ctx.phase() == 2): |
|
71 | elif(self._ctx.phase() == 2): | |
72 | return 'Secret' |
|
72 | return 'Secret' | |
73 | else: |
|
73 | else: | |
74 | return '' |
|
74 | return '' | |
75 |
|
75 | |||
76 | @LazyProperty |
|
76 | @LazyProperty | |
77 | def successors(self): |
|
77 | def successors(self): | |
78 | successors = mercurial.obsutil.successorssets(self._ctx._repo, self._ctx.node(), closest=True) |
|
78 | successors = mercurial.obsutil.successorssets(self._ctx._repo, self._ctx.node(), closest=True) | |
79 | if successors: |
|
79 | if successors: | |
80 | # flatten the list here handles both divergent (len > 1) |
|
80 | # flatten the list here handles both divergent (len > 1) | |
81 | # and the usual case (len = 1) |
|
81 | # and the usual case (len = 1) | |
82 | successors = [mercurial.node.hex(n)[:12] for sub in successors for n in sub if n != self._ctx.node()] |
|
82 | successors = [mercurial.node.hex(n)[:12] for sub in successors for n in sub if n != self._ctx.node()] | |
83 |
|
83 | |||
84 | return successors |
|
84 | return successors | |
85 |
|
85 | |||
86 | @LazyProperty |
|
86 | @LazyProperty | |
87 | def predecessors(self): |
|
87 | def predecessors(self): | |
88 | return [mercurial.node.hex(n)[:12] for n in mercurial.obsutil.closestpredecessors(self._ctx._repo, self._ctx.node())] |
|
88 | return [mercurial.node.hex(n)[:12] for n in mercurial.obsutil.closestpredecessors(self._ctx._repo, self._ctx.node())] | |
89 |
|
89 | |||
90 | @LazyProperty |
|
90 | @LazyProperty | |
91 | def bookmarks(self): |
|
91 | def bookmarks(self): | |
92 | return [safe_str(bookmark) for bookmark in self._ctx.bookmarks()] |
|
92 | return [safe_str(bookmark) for bookmark in self._ctx.bookmarks()] | |
93 |
|
93 | |||
94 | @LazyProperty |
|
94 | @LazyProperty | |
95 | def message(self): |
|
95 | def message(self): | |
96 | return safe_str(self._ctx.description()) |
|
96 | return safe_str(self._ctx.description()) | |
97 |
|
97 | |||
98 | @LazyProperty |
|
98 | @LazyProperty | |
99 | def committer(self): |
|
99 | def committer(self): | |
100 | return safe_str(self.author) |
|
100 | return safe_str(self.author) | |
101 |
|
101 | |||
102 | @LazyProperty |
|
102 | @LazyProperty | |
103 | def author(self): |
|
103 | def author(self): | |
104 | return safe_str(self._ctx.user()) |
|
104 | return safe_str(self._ctx.user()) | |
105 |
|
105 | |||
106 | @LazyProperty |
|
106 | @LazyProperty | |
107 | def date(self): |
|
107 | def date(self): | |
108 | return date_fromtimestamp(*self._ctx.date()) |
|
108 | return date_fromtimestamp(*self._ctx.date()) | |
109 |
|
109 | |||
110 | @LazyProperty |
|
110 | @LazyProperty | |
111 | def _timestamp(self): |
|
111 | def _timestamp(self): | |
112 | return self._ctx.date()[0] |
|
112 | return self._ctx.date()[0] | |
113 |
|
113 | |||
114 | @LazyProperty |
|
114 | @LazyProperty | |
115 | def status(self): |
|
115 | def status(self): | |
116 | """ |
|
116 | """ | |
117 | Returns modified, added, removed, deleted files for current changeset |
|
117 | Returns modified, added, removed, deleted files for current changeset | |
118 | """ |
|
118 | """ | |
119 | return self.repository._repo.status(self._ctx.p1().node(), |
|
119 | return self.repository._repo.status(self._ctx.p1().node(), | |
120 | self._ctx.node()) |
|
120 | self._ctx.node()) | |
121 |
|
121 | |||
122 | @LazyProperty |
|
122 | @LazyProperty | |
123 | def _file_paths(self): |
|
123 | def _file_paths(self): | |
124 | return list(self._ctx) |
|
124 | return list(safe_str(f) for f in self._ctx) | |
125 |
|
125 | |||
126 | @LazyProperty |
|
126 | @LazyProperty | |
127 | def _dir_paths(self): |
|
127 | def _dir_paths(self): | |
128 | p = list(set(get_dirs_for_path(*self._file_paths))) |
|
128 | p = list(set(get_dirs_for_path(*self._file_paths))) | |
129 | p.insert(0, '') |
|
129 | p.insert(0, '') | |
130 | return p |
|
130 | return p | |
131 |
|
131 | |||
132 | @LazyProperty |
|
132 | @LazyProperty | |
133 | def _paths(self): |
|
133 | def _paths(self): | |
134 | return self._dir_paths + self._file_paths |
|
134 | return self._dir_paths + self._file_paths | |
135 |
|
135 | |||
136 | @LazyProperty |
|
136 | @LazyProperty | |
137 | def short_id(self): |
|
137 | def short_id(self): | |
138 | return self.raw_id[:12] |
|
138 | return self.raw_id[:12] | |
139 |
|
139 | |||
140 | @LazyProperty |
|
140 | @LazyProperty | |
141 | def parents(self): |
|
141 | def parents(self): | |
142 | """ |
|
142 | """ | |
143 | Returns list of parents changesets. |
|
143 | Returns list of parents changesets. | |
144 | """ |
|
144 | """ | |
145 | return [self.repository.get_changeset(parent.rev()) |
|
145 | return [self.repository.get_changeset(parent.rev()) | |
146 | for parent in self._ctx.parents() if parent.rev() >= 0] |
|
146 | for parent in self._ctx.parents() if parent.rev() >= 0] | |
147 |
|
147 | |||
148 | @LazyProperty |
|
148 | @LazyProperty | |
149 | def children(self): |
|
149 | def children(self): | |
150 | """ |
|
150 | """ | |
151 | Returns list of children changesets. |
|
151 | Returns list of children changesets. | |
152 | """ |
|
152 | """ | |
153 | return [self.repository.get_changeset(child.rev()) |
|
153 | return [self.repository.get_changeset(child.rev()) | |
154 | for child in self._ctx.children() if child.rev() >= 0] |
|
154 | for child in self._ctx.children() if child.rev() >= 0] | |
155 |
|
155 | |||
156 | def next(self, branch=None): |
|
156 | def next(self, branch=None): | |
157 | if branch and self.branch != branch: |
|
157 | if branch and self.branch != branch: | |
158 | raise VCSError('Branch option used on changeset not belonging ' |
|
158 | raise VCSError('Branch option used on changeset not belonging ' | |
159 | 'to that branch') |
|
159 | 'to that branch') | |
160 |
|
160 | |||
161 | cs = self |
|
161 | cs = self | |
162 | while True: |
|
162 | while True: | |
163 | try: |
|
163 | try: | |
164 | next_ = cs.repository.revisions.index(cs.raw_id) + 1 |
|
164 | next_ = cs.repository.revisions.index(cs.raw_id) + 1 | |
165 | next_rev = cs.repository.revisions[next_] |
|
165 | next_rev = cs.repository.revisions[next_] | |
166 | except IndexError: |
|
166 | except IndexError: | |
167 | raise ChangesetDoesNotExistError |
|
167 | raise ChangesetDoesNotExistError | |
168 | cs = cs.repository.get_changeset(next_rev) |
|
168 | cs = cs.repository.get_changeset(next_rev) | |
169 |
|
169 | |||
170 | if not branch or branch == cs.branch: |
|
170 | if not branch or branch == cs.branch: | |
171 | return cs |
|
171 | return cs | |
172 |
|
172 | |||
173 | def prev(self, branch=None): |
|
173 | def prev(self, branch=None): | |
174 | if branch and self.branch != branch: |
|
174 | if branch and self.branch != branch: | |
175 | raise VCSError('Branch option used on changeset not belonging ' |
|
175 | raise VCSError('Branch option used on changeset not belonging ' | |
176 | 'to that branch') |
|
176 | 'to that branch') | |
177 |
|
177 | |||
178 | cs = self |
|
178 | cs = self | |
179 | while True: |
|
179 | while True: | |
180 | try: |
|
180 | try: | |
181 | prev_ = cs.repository.revisions.index(cs.raw_id) - 1 |
|
181 | prev_ = cs.repository.revisions.index(cs.raw_id) - 1 | |
182 | if prev_ < 0: |
|
182 | if prev_ < 0: | |
183 | raise IndexError |
|
183 | raise IndexError | |
184 | prev_rev = cs.repository.revisions[prev_] |
|
184 | prev_rev = cs.repository.revisions[prev_] | |
185 | except IndexError: |
|
185 | except IndexError: | |
186 | raise ChangesetDoesNotExistError |
|
186 | raise ChangesetDoesNotExistError | |
187 | cs = cs.repository.get_changeset(prev_rev) |
|
187 | cs = cs.repository.get_changeset(prev_rev) | |
188 |
|
188 | |||
189 | if not branch or branch == cs.branch: |
|
189 | if not branch or branch == cs.branch: | |
190 | return cs |
|
190 | return cs | |
191 |
|
191 | |||
192 | def diff(self): |
|
192 | def diff(self): | |
193 | # Only used to feed diffstat |
|
193 | # Only used to feed diffstat | |
194 | return b''.join(self._ctx.diff()) |
|
194 | return b''.join(self._ctx.diff()) | |
195 |
|
195 | |||
196 | def _fix_path(self, path): |
|
196 | def _fix_path(self, path): | |
197 | """ |
|
197 | """ | |
198 | Paths are stored without trailing slash so we need to get rid off it if |
|
198 | Paths are stored without trailing slash so we need to get rid off it if | |
199 | needed. Also mercurial keeps filenodes as str so we need to decode |
|
199 | needed. Also mercurial keeps filenodes as str so we need to decode | |
200 | from unicode to str |
|
200 | from unicode to str | |
201 | """ |
|
201 | """ | |
202 | if path.endswith('/'): |
|
202 | if path.endswith('/'): | |
203 | path = path.rstrip('/') |
|
203 | path = path.rstrip('/') | |
204 |
|
204 | |||
205 | return path |
|
205 | return path | |
206 |
|
206 | |||
207 | def _get_kind(self, path): |
|
207 | def _get_kind(self, path): | |
208 | path = self._fix_path(path) |
|
208 | path = self._fix_path(path) | |
209 | if path in self._file_paths: |
|
209 | if path in self._file_paths: | |
210 | return NodeKind.FILE |
|
210 | return NodeKind.FILE | |
211 | elif path in self._dir_paths: |
|
211 | elif path in self._dir_paths: | |
212 | return NodeKind.DIR |
|
212 | return NodeKind.DIR | |
213 | else: |
|
213 | else: | |
214 | raise ChangesetError("Node does not exist at the given path '%s'" |
|
214 | raise ChangesetError("Node does not exist at the given path '%s'" | |
215 | % (path)) |
|
215 | % (path)) | |
216 |
|
216 | |||
217 | def _get_filectx(self, path): |
|
217 | def _get_filectx(self, path): | |
218 | path = self._fix_path(path) |
|
218 | path = self._fix_path(path) | |
219 | if self._get_kind(path) != NodeKind.FILE: |
|
219 | if self._get_kind(path) != NodeKind.FILE: | |
220 | raise ChangesetError("File does not exist for revision %s at " |
|
220 | raise ChangesetError("File does not exist for revision %s at " | |
221 | " '%s'" % (self.raw_id, path)) |
|
221 | " '%s'" % (self.raw_id, path)) | |
222 | return self._ctx.filectx(safe_bytes(path)) |
|
222 | return self._ctx.filectx(safe_bytes(path)) | |
223 |
|
223 | |||
224 | def _extract_submodules(self): |
|
224 | def _extract_submodules(self): | |
225 | """ |
|
225 | """ | |
226 | returns a dictionary with submodule information from substate file |
|
226 | returns a dictionary with submodule information from substate file | |
227 | of hg repository |
|
227 | of hg repository | |
228 | """ |
|
228 | """ | |
229 | return self._ctx.substate |
|
229 | return self._ctx.substate | |
230 |
|
230 | |||
231 | def get_file_mode(self, path): |
|
231 | def get_file_mode(self, path): | |
232 | """ |
|
232 | """ | |
233 | Returns stat mode of the file at the given ``path``. |
|
233 | Returns stat mode of the file at the given ``path``. | |
234 | """ |
|
234 | """ | |
235 | fctx = self._get_filectx(path) |
|
235 | fctx = self._get_filectx(path) | |
236 | if b'x' in fctx.flags(): |
|
236 | if b'x' in fctx.flags(): | |
237 | return 0o100755 |
|
237 | return 0o100755 | |
238 | else: |
|
238 | else: | |
239 | return 0o100644 |
|
239 | return 0o100644 | |
240 |
|
240 | |||
241 | def get_file_content(self, path): |
|
241 | def get_file_content(self, path): | |
242 | """ |
|
242 | """ | |
243 | Returns content of the file at given ``path``. |
|
243 | Returns content of the file at given ``path``. | |
244 | """ |
|
244 | """ | |
245 | fctx = self._get_filectx(path) |
|
245 | fctx = self._get_filectx(path) | |
246 | return fctx.data() |
|
246 | return fctx.data() | |
247 |
|
247 | |||
248 | def get_file_size(self, path): |
|
248 | def get_file_size(self, path): | |
249 | """ |
|
249 | """ | |
250 | Returns size of the file at given ``path``. |
|
250 | Returns size of the file at given ``path``. | |
251 | """ |
|
251 | """ | |
252 | fctx = self._get_filectx(path) |
|
252 | fctx = self._get_filectx(path) | |
253 | return fctx.size() |
|
253 | return fctx.size() | |
254 |
|
254 | |||
255 | def get_file_changeset(self, path): |
|
255 | def get_file_changeset(self, path): | |
256 | """ |
|
256 | """ | |
257 | Returns last commit of the file at the given ``path``. |
|
257 | Returns last commit of the file at the given ``path``. | |
258 | """ |
|
258 | """ | |
259 | return self.get_file_history(path, limit=1)[0] |
|
259 | return self.get_file_history(path, limit=1)[0] | |
260 |
|
260 | |||
261 | def get_file_history(self, path, limit=None): |
|
261 | def get_file_history(self, path, limit=None): | |
262 | """ |
|
262 | """ | |
263 | Returns history of file as reversed list of ``Changeset`` objects for |
|
263 | Returns history of file as reversed list of ``Changeset`` objects for | |
264 | which file at given ``path`` has been modified. |
|
264 | which file at given ``path`` has been modified. | |
265 | """ |
|
265 | """ | |
266 | fctx = self._get_filectx(path) |
|
266 | fctx = self._get_filectx(path) | |
267 | hist = [] |
|
267 | hist = [] | |
268 | cnt = 0 |
|
268 | cnt = 0 | |
269 | for cs in reversed([x for x in fctx.filelog()]): |
|
269 | for cs in reversed([x for x in fctx.filelog()]): | |
270 | cnt += 1 |
|
270 | cnt += 1 | |
271 | hist.append(mercurial.node.hex(fctx.filectx(cs).node())) |
|
271 | hist.append(mercurial.node.hex(fctx.filectx(cs).node())) | |
272 | if limit is not None and cnt == limit: |
|
272 | if limit is not None and cnt == limit: | |
273 | break |
|
273 | break | |
274 |
|
274 | |||
275 | return [self.repository.get_changeset(node) for node in hist] |
|
275 | return [self.repository.get_changeset(node) for node in hist] | |
276 |
|
276 | |||
277 | def get_file_annotate(self, path): |
|
277 | def get_file_annotate(self, path): | |
278 | """ |
|
278 | """ | |
279 | Returns a generator of four element tuples with |
|
279 | Returns a generator of four element tuples with | |
280 | lineno, sha, changeset lazy loader and line |
|
280 | lineno, sha, changeset lazy loader and line | |
281 | """ |
|
281 | """ | |
282 | annotations = self._get_filectx(path).annotate() |
|
282 | annotations = self._get_filectx(path).annotate() | |
283 | annotation_lines = [(annotateline.fctx, annotateline.text) for annotateline in annotations] |
|
283 | annotation_lines = [(annotateline.fctx, annotateline.text) for annotateline in annotations] | |
284 | for i, (fctx, line) in enumerate(annotation_lines): |
|
284 | for i, (fctx, line) in enumerate(annotation_lines): | |
285 | sha = ascii_str(fctx.hex()) |
|
285 | sha = ascii_str(fctx.hex()) | |
286 | yield (i + 1, sha, lambda sha=sha: self.repository.get_changeset(sha), line) |
|
286 | yield (i + 1, sha, lambda sha=sha: self.repository.get_changeset(sha), line) | |
287 |
|
287 | |||
288 | def fill_archive(self, stream=None, kind='tgz', prefix=None, |
|
288 | def fill_archive(self, stream=None, kind='tgz', prefix=None, | |
289 | subrepos=False): |
|
289 | subrepos=False): | |
290 | """ |
|
290 | """ | |
291 | Fills up given stream. |
|
291 | Fills up given stream. | |
292 |
|
292 | |||
293 | :param stream: file like object. |
|
293 | :param stream: file like object. | |
294 | :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``. |
|
294 | :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``. | |
295 | Default: ``tgz``. |
|
295 | Default: ``tgz``. | |
296 | :param prefix: name of root directory in archive. |
|
296 | :param prefix: name of root directory in archive. | |
297 | Default is repository name and changeset's raw_id joined with dash |
|
297 | Default is repository name and changeset's raw_id joined with dash | |
298 | (``repo-tip.<KIND>``). |
|
298 | (``repo-tip.<KIND>``). | |
299 | :param subrepos: include subrepos in this archive. |
|
299 | :param subrepos: include subrepos in this archive. | |
300 |
|
300 | |||
301 | :raise ImproperArchiveTypeError: If given kind is wrong. |
|
301 | :raise ImproperArchiveTypeError: If given kind is wrong. | |
302 | :raise VcsError: If given stream is None |
|
302 | :raise VcsError: If given stream is None | |
303 | """ |
|
303 | """ | |
304 | allowed_kinds = settings.ARCHIVE_SPECS |
|
304 | allowed_kinds = settings.ARCHIVE_SPECS | |
305 | if kind not in allowed_kinds: |
|
305 | if kind not in allowed_kinds: | |
306 | raise ImproperArchiveTypeError('Archive kind not supported use one' |
|
306 | raise ImproperArchiveTypeError('Archive kind not supported use one' | |
307 | 'of %s' % ' '.join(allowed_kinds)) |
|
307 | 'of %s' % ' '.join(allowed_kinds)) | |
308 |
|
308 | |||
309 | if stream is None: |
|
309 | if stream is None: | |
310 | raise VCSError('You need to pass in a valid stream for filling' |
|
310 | raise VCSError('You need to pass in a valid stream for filling' | |
311 | ' with archival data') |
|
311 | ' with archival data') | |
312 |
|
312 | |||
313 | if prefix is None: |
|
313 | if prefix is None: | |
314 | prefix = '%s-%s' % (self.repository.name, self.short_id) |
|
314 | prefix = '%s-%s' % (self.repository.name, self.short_id) | |
315 | elif prefix.startswith('/'): |
|
315 | elif prefix.startswith('/'): | |
316 | raise VCSError("Prefix cannot start with leading slash") |
|
316 | raise VCSError("Prefix cannot start with leading slash") | |
317 | elif prefix.strip() == '': |
|
317 | elif prefix.strip() == '': | |
318 | raise VCSError("Prefix cannot be empty") |
|
318 | raise VCSError("Prefix cannot be empty") | |
319 |
|
319 | |||
320 | mercurial.archival.archive(self.repository._repo, stream, ascii_bytes(self.raw_id), |
|
320 | mercurial.archival.archive(self.repository._repo, stream, ascii_bytes(self.raw_id), | |
321 | safe_bytes(kind), prefix=safe_bytes(prefix), subrepos=subrepos) |
|
321 | safe_bytes(kind), prefix=safe_bytes(prefix), subrepos=subrepos) | |
322 |
|
322 | |||
323 | def get_nodes(self, path): |
|
323 | def get_nodes(self, path): | |
324 | """ |
|
324 | """ | |
325 | Returns combined ``DirNode`` and ``FileNode`` objects list representing |
|
325 | Returns combined ``DirNode`` and ``FileNode`` objects list representing | |
326 | state of changeset at the given ``path``. If node at the given ``path`` |
|
326 | state of changeset at the given ``path``. If node at the given ``path`` | |
327 | is not instance of ``DirNode``, ChangesetError would be raised. |
|
327 | is not instance of ``DirNode``, ChangesetError would be raised. | |
328 | """ |
|
328 | """ | |
329 |
|
329 | |||
330 | if self._get_kind(path) != NodeKind.DIR: |
|
330 | if self._get_kind(path) != NodeKind.DIR: | |
331 | raise ChangesetError("Directory does not exist for revision %s at " |
|
331 | raise ChangesetError("Directory does not exist for revision %s at " | |
332 | " '%s'" % (self.revision, path)) |
|
332 | " '%s'" % (self.revision, path)) | |
333 | path = self._fix_path(path) |
|
333 | path = self._fix_path(path) | |
334 |
|
334 | |||
335 | filenodes = [FileNode(f, changeset=self) for f in self._file_paths |
|
335 | filenodes = [FileNode(f, changeset=self) for f in self._file_paths | |
336 | if os.path.dirname(f) == path] |
|
336 | if os.path.dirname(f) == path] | |
337 | dirs = path == '' and '' or [d for d in self._dir_paths |
|
337 | dirs = path == '' and '' or [d for d in self._dir_paths | |
338 | if d and posixpath.dirname(d) == path] |
|
338 | if d and posixpath.dirname(d) == path] | |
339 | dirnodes = [DirNode(d, changeset=self) for d in dirs |
|
339 | dirnodes = [DirNode(d, changeset=self) for d in dirs | |
340 | if os.path.dirname(d) == path] |
|
340 | if os.path.dirname(d) == path] | |
341 |
|
341 | |||
342 | als = self.repository.alias |
|
342 | als = self.repository.alias | |
343 | for k, vals in self._extract_submodules().items(): |
|
343 | for k, vals in self._extract_submodules().items(): | |
344 | #vals = url,rev,type |
|
344 | #vals = url,rev,type | |
345 | loc = vals[0] |
|
345 | loc = vals[0] | |
346 | cs = vals[1] |
|
346 | cs = vals[1] | |
347 | dirnodes.append(SubModuleNode(k, url=loc, changeset=cs, |
|
347 | dirnodes.append(SubModuleNode(k, url=loc, changeset=cs, | |
348 | alias=als)) |
|
348 | alias=als)) | |
349 | nodes = dirnodes + filenodes |
|
349 | nodes = dirnodes + filenodes | |
350 | for node in nodes: |
|
350 | for node in nodes: | |
351 | self.nodes[node.path] = node |
|
351 | self.nodes[node.path] = node | |
352 | nodes.sort() |
|
352 | nodes.sort() | |
353 | return nodes |
|
353 | return nodes | |
354 |
|
354 | |||
355 | def get_node(self, path): |
|
355 | def get_node(self, path): | |
356 | """ |
|
356 | """ | |
357 | Returns ``Node`` object from the given ``path``. If there is no node at |
|
357 | Returns ``Node`` object from the given ``path``. If there is no node at | |
358 | the given ``path``, ``ChangesetError`` would be raised. |
|
358 | the given ``path``, ``ChangesetError`` would be raised. | |
359 | """ |
|
359 | """ | |
360 | path = self._fix_path(path) |
|
360 | path = self._fix_path(path) | |
361 | if path not in self.nodes: |
|
361 | if path not in self.nodes: | |
362 | if path in self._file_paths: |
|
362 | if path in self._file_paths: | |
363 | node = FileNode(path, changeset=self) |
|
363 | node = FileNode(path, changeset=self) | |
364 | elif path in self._dir_paths or path in self._dir_paths: |
|
364 | elif path in self._dir_paths or path in self._dir_paths: | |
365 | if path == '': |
|
365 | if path == '': | |
366 | node = RootNode(changeset=self) |
|
366 | node = RootNode(changeset=self) | |
367 | else: |
|
367 | else: | |
368 | node = DirNode(path, changeset=self) |
|
368 | node = DirNode(path, changeset=self) | |
369 | else: |
|
369 | else: | |
370 | raise NodeDoesNotExistError("There is no file nor directory " |
|
370 | raise NodeDoesNotExistError("There is no file nor directory " | |
371 | "at the given path: '%s' at revision %s" |
|
371 | "at the given path: '%s' at revision %s" | |
372 | % (path, self.short_id)) |
|
372 | % (path, self.short_id)) | |
373 | # cache node |
|
373 | # cache node | |
374 | self.nodes[path] = node |
|
374 | self.nodes[path] = node | |
375 | return self.nodes[path] |
|
375 | return self.nodes[path] | |
376 |
|
376 | |||
377 | @LazyProperty |
|
377 | @LazyProperty | |
378 | def affected_files(self): |
|
378 | def affected_files(self): | |
379 | """ |
|
379 | """ | |
380 | Gets a fast accessible file changes for given changeset |
|
380 | Gets a fast accessible file changes for given changeset | |
381 | """ |
|
381 | """ | |
382 | return self._ctx.files() |
|
382 | return self._ctx.files() | |
383 |
|
383 | |||
384 | @property |
|
384 | @property | |
385 | def added(self): |
|
385 | def added(self): | |
386 | """ |
|
386 | """ | |
387 | Returns list of added ``FileNode`` objects. |
|
387 | Returns list of added ``FileNode`` objects. | |
388 | """ |
|
388 | """ | |
389 | return AddedFileNodesGenerator([n for n in self.status.added], self) |
|
389 | return AddedFileNodesGenerator([safe_str(n) for n in self.status.added], self) | |
390 |
|
390 | |||
391 | @property |
|
391 | @property | |
392 | def changed(self): |
|
392 | def changed(self): | |
393 | """ |
|
393 | """ | |
394 | Returns list of modified ``FileNode`` objects. |
|
394 | Returns list of modified ``FileNode`` objects. | |
395 | """ |
|
395 | """ | |
396 | return ChangedFileNodesGenerator([n for n in self.status.modified], self) |
|
396 | return ChangedFileNodesGenerator([safe_str(n) for n in self.status.modified], self) | |
397 |
|
397 | |||
398 | @property |
|
398 | @property | |
399 | def removed(self): |
|
399 | def removed(self): | |
400 | """ |
|
400 | """ | |
401 | Returns list of removed ``FileNode`` objects. |
|
401 | Returns list of removed ``FileNode`` objects. | |
402 | """ |
|
402 | """ | |
403 | return RemovedFileNodesGenerator([n for n in self.status.removed], self) |
|
403 | return RemovedFileNodesGenerator([safe_str(n) for n in self.status.removed], self) | |
404 |
|
404 | |||
405 | @LazyProperty |
|
405 | @LazyProperty | |
406 | def extra(self): |
|
406 | def extra(self): | |
407 | return self._ctx.extra() |
|
407 | return self._ctx.extra() |
@@ -1,110 +1,110 b'' | |||||
1 | import datetime |
|
1 | import datetime | |
2 |
|
2 | |||
3 | import mercurial.context |
|
3 | import mercurial.context | |
4 | import mercurial.node |
|
4 | import mercurial.node | |
5 |
|
5 | |||
6 | from kallithea.lib.vcs.backends.base import BaseInMemoryChangeset |
|
6 | from kallithea.lib.vcs.backends.base import BaseInMemoryChangeset | |
7 | from kallithea.lib.vcs.exceptions import RepositoryError |
|
7 | from kallithea.lib.vcs.exceptions import RepositoryError | |
8 | from kallithea.lib.vcs.utils import ascii_str, safe_bytes |
|
8 | from kallithea.lib.vcs.utils import ascii_str, safe_bytes, safe_str | |
9 |
|
9 | |||
10 |
|
10 | |||
11 | class MercurialInMemoryChangeset(BaseInMemoryChangeset): |
|
11 | class MercurialInMemoryChangeset(BaseInMemoryChangeset): | |
12 |
|
12 | |||
13 | def commit(self, message, author, parents=None, branch=None, date=None, |
|
13 | def commit(self, message, author, parents=None, branch=None, date=None, | |
14 | **kwargs): |
|
14 | **kwargs): | |
15 | """ |
|
15 | """ | |
16 | Performs in-memory commit (doesn't check workdir in any way) and |
|
16 | Performs in-memory commit (doesn't check workdir in any way) and | |
17 | returns newly created ``Changeset``. Updates repository's |
|
17 | returns newly created ``Changeset``. Updates repository's | |
18 | ``revisions``. |
|
18 | ``revisions``. | |
19 |
|
19 | |||
20 | :param message: message of the commit |
|
20 | :param message: message of the commit | |
21 | :param author: full username, i.e. "Joe Doe <joe.doe@example.com>" |
|
21 | :param author: full username, i.e. "Joe Doe <joe.doe@example.com>" | |
22 | :param parents: single parent or sequence of parents from which commit |
|
22 | :param parents: single parent or sequence of parents from which commit | |
23 | would be derived |
|
23 | would be derived | |
24 | :param date: ``datetime.datetime`` instance. Defaults to |
|
24 | :param date: ``datetime.datetime`` instance. Defaults to | |
25 | ``datetime.datetime.now()``. |
|
25 | ``datetime.datetime.now()``. | |
26 | :param branch: branch name, as string. If none given, default backend's |
|
26 | :param branch: branch name, as string. If none given, default backend's | |
27 | branch would be used. |
|
27 | branch would be used. | |
28 |
|
28 | |||
29 | :raises ``CommitError``: if any error occurs while committing |
|
29 | :raises ``CommitError``: if any error occurs while committing | |
30 | """ |
|
30 | """ | |
31 | self.check_integrity(parents) |
|
31 | self.check_integrity(parents) | |
32 |
|
32 | |||
33 | from .repository import MercurialRepository |
|
33 | from .repository import MercurialRepository | |
34 | if not isinstance(message, unicode) or not isinstance(author, unicode): |
|
34 | if not isinstance(message, unicode) or not isinstance(author, unicode): | |
35 | raise RepositoryError('Given message and author needs to be ' |
|
35 | raise RepositoryError('Given message and author needs to be ' | |
36 | 'an <unicode> instance got %r & %r instead' |
|
36 | 'an <unicode> instance got %r & %r instead' | |
37 | % (type(message), type(author))) |
|
37 | % (type(message), type(author))) | |
38 |
|
38 | |||
39 | if branch is None: |
|
39 | if branch is None: | |
40 | branch = MercurialRepository.DEFAULT_BRANCH_NAME |
|
40 | branch = MercurialRepository.DEFAULT_BRANCH_NAME | |
41 | kwargs[b'branch'] = safe_bytes(branch) |
|
41 | kwargs[b'branch'] = safe_bytes(branch) | |
42 |
|
42 | |||
43 | def filectxfn(_repo, memctx, bytes_path): |
|
43 | def filectxfn(_repo, memctx, bytes_path): | |
44 | """ |
|
44 | """ | |
45 | Callback from Mercurial, returning ctx to commit for the given |
|
45 | Callback from Mercurial, returning ctx to commit for the given | |
46 | path. |
|
46 | path. | |
47 | """ |
|
47 | """ | |
48 | path = bytes_path # will be different for py3 |
|
48 | path = safe_str(bytes_path) | |
49 |
|
49 | |||
50 | # check if this path is removed |
|
50 | # check if this path is removed | |
51 | if path in (node.path for node in self.removed): |
|
51 | if path in (node.path for node in self.removed): | |
52 | return None |
|
52 | return None | |
53 |
|
53 | |||
54 | # check if this path is added |
|
54 | # check if this path is added | |
55 | for node in self.added: |
|
55 | for node in self.added: | |
56 | if node.path == path: |
|
56 | if node.path == path: | |
57 | return mercurial.context.memfilectx(_repo, memctx, path=bytes_path, |
|
57 | return mercurial.context.memfilectx(_repo, memctx, path=bytes_path, | |
58 | data=node.content, |
|
58 | data=node.content, | |
59 | islink=False, |
|
59 | islink=False, | |
60 | isexec=node.is_executable, |
|
60 | isexec=node.is_executable, | |
61 | copysource=False) |
|
61 | copysource=False) | |
62 |
|
62 | |||
63 | # or changed |
|
63 | # or changed | |
64 | for node in self.changed: |
|
64 | for node in self.changed: | |
65 | if node.path == path: |
|
65 | if node.path == path: | |
66 | return mercurial.context.memfilectx(_repo, memctx, path=bytes_path, |
|
66 | return mercurial.context.memfilectx(_repo, memctx, path=bytes_path, | |
67 | data=node.content, |
|
67 | data=node.content, | |
68 | islink=False, |
|
68 | islink=False, | |
69 | isexec=node.is_executable, |
|
69 | isexec=node.is_executable, | |
70 | copysource=False) |
|
70 | copysource=False) | |
71 |
|
71 | |||
72 | raise RepositoryError("Given path haven't been marked as added, " |
|
72 | raise RepositoryError("Given path haven't been marked as added, " | |
73 | "changed or removed (%s)" % path) |
|
73 | "changed or removed (%s)" % path) | |
74 |
|
74 | |||
75 | parents = [None, None] |
|
75 | parents = [None, None] | |
76 | for i, parent in enumerate(self.parents): |
|
76 | for i, parent in enumerate(self.parents): | |
77 | if parent is not None: |
|
77 | if parent is not None: | |
78 | parents[i] = parent._ctx.node() |
|
78 | parents[i] = parent._ctx.node() | |
79 |
|
79 | |||
80 | if date and isinstance(date, datetime.datetime): |
|
80 | if date and isinstance(date, datetime.datetime): | |
81 | date = safe_bytes(date.strftime('%a, %d %b %Y %H:%M:%S')) |
|
81 | date = safe_bytes(date.strftime('%a, %d %b %Y %H:%M:%S')) | |
82 |
|
82 | |||
83 | commit_ctx = mercurial.context.memctx( |
|
83 | commit_ctx = mercurial.context.memctx( | |
84 | repo=self.repository._repo, |
|
84 | repo=self.repository._repo, | |
85 | parents=parents, |
|
85 | parents=parents, | |
86 | text=b'', |
|
86 | text=b'', | |
87 | files=[safe_bytes(x) for x in self.get_paths()], |
|
87 | files=[safe_bytes(x) for x in self.get_paths()], | |
88 | filectxfn=filectxfn, |
|
88 | filectxfn=filectxfn, | |
89 | user=safe_bytes(author), |
|
89 | user=safe_bytes(author), | |
90 | date=date, |
|
90 | date=date, | |
91 | extra=kwargs) |
|
91 | extra=kwargs) | |
92 |
|
92 | |||
93 | # injecting given _repo params |
|
93 | # injecting given _repo params | |
94 | commit_ctx._text = safe_bytes(message) |
|
94 | commit_ctx._text = safe_bytes(message) | |
95 | commit_ctx._user = safe_bytes(author) |
|
95 | commit_ctx._user = safe_bytes(author) | |
96 | commit_ctx._date = date |
|
96 | commit_ctx._date = date | |
97 |
|
97 | |||
98 | # TODO: Catch exceptions! |
|
98 | # TODO: Catch exceptions! | |
99 | n = self.repository._repo.commitctx(commit_ctx) |
|
99 | n = self.repository._repo.commitctx(commit_ctx) | |
100 | # Returns mercurial node |
|
100 | # Returns mercurial node | |
101 | self._commit_ctx = commit_ctx # For reference |
|
101 | self._commit_ctx = commit_ctx # For reference | |
102 | # Update vcs repository object & recreate mercurial _repo |
|
102 | # Update vcs repository object & recreate mercurial _repo | |
103 | # new_ctx = self.repository._repo[node] |
|
103 | # new_ctx = self.repository._repo[node] | |
104 | # new_tip = ascii_str(self.repository.get_changeset(new_ctx.hex())) |
|
104 | # new_tip = ascii_str(self.repository.get_changeset(new_ctx.hex())) | |
105 | self.repository.revisions.append(ascii_str(mercurial.node.hex(n))) |
|
105 | self.repository.revisions.append(ascii_str(mercurial.node.hex(n))) | |
106 | self._repo = self.repository._get_repo(create=False) |
|
106 | self._repo = self.repository._get_repo(create=False) | |
107 | self.repository.branches = self.repository._get_branches() |
|
107 | self.repository.branches = self.repository._get_branches() | |
108 | tip = self.repository.get_changeset() |
|
108 | tip = self.repository.get_changeset() | |
109 | self.reset() |
|
109 | self.reset() | |
110 | return tip |
|
110 | return tip |
@@ -1,616 +1,617 b'' | |||||
1 | # -*- coding: utf-8 -*- |
|
1 | # -*- coding: utf-8 -*- | |
2 | """ |
|
2 | """ | |
3 | vcs.backends.hg.repository |
|
3 | vcs.backends.hg.repository | |
4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
5 |
|
5 | |||
6 | Mercurial repository implementation. |
|
6 | Mercurial repository implementation. | |
7 |
|
7 | |||
8 | :created_on: Apr 8, 2010 |
|
8 | :created_on: Apr 8, 2010 | |
9 | :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak. |
|
9 | :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak. | |
10 | """ |
|
10 | """ | |
11 |
|
11 | |||
12 | import datetime |
|
12 | import datetime | |
13 | import logging |
|
13 | import logging | |
14 | import os |
|
14 | import os | |
15 | import time |
|
15 | import time | |
16 | import urllib.error |
|
16 | import urllib.error | |
17 | import urllib.parse |
|
17 | import urllib.parse | |
18 | import urllib.request |
|
18 | import urllib.request | |
19 | from collections import OrderedDict |
|
19 | from collections import OrderedDict | |
20 |
|
20 | |||
21 | import mercurial.commands |
|
21 | import mercurial.commands | |
22 | import mercurial.error |
|
22 | import mercurial.error | |
23 | import mercurial.exchange |
|
23 | import mercurial.exchange | |
24 | import mercurial.hg |
|
24 | import mercurial.hg | |
25 | import mercurial.hgweb |
|
25 | import mercurial.hgweb | |
26 | import mercurial.httppeer |
|
26 | import mercurial.httppeer | |
27 | import mercurial.localrepo |
|
27 | import mercurial.localrepo | |
28 | import mercurial.match |
|
28 | import mercurial.match | |
29 | import mercurial.mdiff |
|
29 | import mercurial.mdiff | |
30 | import mercurial.node |
|
30 | import mercurial.node | |
31 | import mercurial.patch |
|
31 | import mercurial.patch | |
32 | import mercurial.scmutil |
|
32 | import mercurial.scmutil | |
33 | import mercurial.sshpeer |
|
33 | import mercurial.sshpeer | |
34 | import mercurial.tags |
|
34 | import mercurial.tags | |
35 | import mercurial.ui |
|
35 | import mercurial.ui | |
36 | import mercurial.url |
|
36 | import mercurial.url | |
37 | import mercurial.util |
|
37 | import mercurial.util | |
38 |
|
38 | |||
39 | from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator |
|
39 | from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator | |
40 | from kallithea.lib.vcs.exceptions import ( |
|
40 | from kallithea.lib.vcs.exceptions import ( | |
41 | BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError) |
|
41 | BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError) | |
42 | from kallithea.lib.vcs.utils import ascii_str, author_email, author_name, date_fromtimestamp, makedate, safe_bytes, safe_str |
|
42 | from kallithea.lib.vcs.utils import ascii_str, author_email, author_name, date_fromtimestamp, makedate, safe_bytes, safe_str | |
43 | from kallithea.lib.vcs.utils.lazy import LazyProperty |
|
43 | from kallithea.lib.vcs.utils.lazy import LazyProperty | |
44 | from kallithea.lib.vcs.utils.paths import abspath |
|
44 | from kallithea.lib.vcs.utils.paths import abspath | |
45 |
|
45 | |||
46 | from .changeset import MercurialChangeset |
|
46 | from .changeset import MercurialChangeset | |
47 | from .inmemory import MercurialInMemoryChangeset |
|
47 | from .inmemory import MercurialInMemoryChangeset | |
48 | from .workdir import MercurialWorkdir |
|
48 | from .workdir import MercurialWorkdir | |
49 |
|
49 | |||
50 |
|
50 | |||
51 | log = logging.getLogger(__name__) |
|
51 | log = logging.getLogger(__name__) | |
52 |
|
52 | |||
53 |
|
53 | |||
54 | class MercurialRepository(BaseRepository): |
|
54 | class MercurialRepository(BaseRepository): | |
55 | """ |
|
55 | """ | |
56 | Mercurial repository backend |
|
56 | Mercurial repository backend | |
57 | """ |
|
57 | """ | |
58 | DEFAULT_BRANCH_NAME = 'default' |
|
58 | DEFAULT_BRANCH_NAME = 'default' | |
59 | scm = 'hg' |
|
59 | scm = 'hg' | |
60 |
|
60 | |||
61 | def __init__(self, repo_path, create=False, baseui=None, src_url=None, |
|
61 | def __init__(self, repo_path, create=False, baseui=None, src_url=None, | |
62 | update_after_clone=False): |
|
62 | update_after_clone=False): | |
63 | """ |
|
63 | """ | |
64 | Raises RepositoryError if repository could not be find at the given |
|
64 | Raises RepositoryError if repository could not be find at the given | |
65 | ``repo_path``. |
|
65 | ``repo_path``. | |
66 |
|
66 | |||
67 | :param repo_path: local path of the repository |
|
67 | :param repo_path: local path of the repository | |
68 | :param create=False: if set to True, would try to create repository if |
|
68 | :param create=False: if set to True, would try to create repository if | |
69 | it does not exist rather than raising exception |
|
69 | it does not exist rather than raising exception | |
70 | :param baseui=None: user data |
|
70 | :param baseui=None: user data | |
71 | :param src_url=None: would try to clone repository from given location |
|
71 | :param src_url=None: would try to clone repository from given location | |
72 | :param update_after_clone=False: sets update of working copy after |
|
72 | :param update_after_clone=False: sets update of working copy after | |
73 | making a clone |
|
73 | making a clone | |
74 | """ |
|
74 | """ | |
75 |
|
75 | |||
76 | if not isinstance(repo_path, str): |
|
76 | if not isinstance(repo_path, str): | |
77 | raise VCSError('Mercurial backend requires repository path to ' |
|
77 | raise VCSError('Mercurial backend requires repository path to ' | |
78 | 'be instance of <str> got %s instead' % |
|
78 | 'be instance of <str> got %s instead' % | |
79 | type(repo_path)) |
|
79 | type(repo_path)) | |
80 | self.path = abspath(repo_path) |
|
80 | self.path = abspath(repo_path) | |
81 | self.baseui = baseui or mercurial.ui.ui() |
|
81 | self.baseui = baseui or mercurial.ui.ui() | |
82 | # We've set path and ui, now we can set _repo itself |
|
82 | # We've set path and ui, now we can set _repo itself | |
83 | self._repo = self._get_repo(create, src_url, update_after_clone) |
|
83 | self._repo = self._get_repo(create, src_url, update_after_clone) | |
84 |
|
84 | |||
85 | @property |
|
85 | @property | |
86 | def _empty(self): |
|
86 | def _empty(self): | |
87 | """ |
|
87 | """ | |
88 | Checks if repository is empty ie. without any changesets |
|
88 | Checks if repository is empty ie. without any changesets | |
89 | """ |
|
89 | """ | |
90 | # TODO: Following raises errors when using InMemoryChangeset... |
|
90 | # TODO: Following raises errors when using InMemoryChangeset... | |
91 | # return len(self._repo.changelog) == 0 |
|
91 | # return len(self._repo.changelog) == 0 | |
92 | return len(self.revisions) == 0 |
|
92 | return len(self.revisions) == 0 | |
93 |
|
93 | |||
94 | @LazyProperty |
|
94 | @LazyProperty | |
95 | def revisions(self): |
|
95 | def revisions(self): | |
96 | """ |
|
96 | """ | |
97 | Returns list of revisions' ids, in ascending order. Being lazy |
|
97 | Returns list of revisions' ids, in ascending order. Being lazy | |
98 | attribute allows external tools to inject shas from cache. |
|
98 | attribute allows external tools to inject shas from cache. | |
99 | """ |
|
99 | """ | |
100 | return self._get_all_revisions() |
|
100 | return self._get_all_revisions() | |
101 |
|
101 | |||
102 | @LazyProperty |
|
102 | @LazyProperty | |
103 | def name(self): |
|
103 | def name(self): | |
104 | return os.path.basename(self.path) |
|
104 | return os.path.basename(self.path) | |
105 |
|
105 | |||
106 | @LazyProperty |
|
106 | @LazyProperty | |
107 | def branches(self): |
|
107 | def branches(self): | |
108 | return self._get_branches() |
|
108 | return self._get_branches() | |
109 |
|
109 | |||
110 | @LazyProperty |
|
110 | @LazyProperty | |
111 | def closed_branches(self): |
|
111 | def closed_branches(self): | |
112 | return self._get_branches(normal=False, closed=True) |
|
112 | return self._get_branches(normal=False, closed=True) | |
113 |
|
113 | |||
114 | @LazyProperty |
|
114 | @LazyProperty | |
115 | def allbranches(self): |
|
115 | def allbranches(self): | |
116 | """ |
|
116 | """ | |
117 | List all branches, including closed branches. |
|
117 | List all branches, including closed branches. | |
118 | """ |
|
118 | """ | |
119 | return self._get_branches(closed=True) |
|
119 | return self._get_branches(closed=True) | |
120 |
|
120 | |||
121 | def _get_branches(self, normal=True, closed=False): |
|
121 | def _get_branches(self, normal=True, closed=False): | |
122 | """ |
|
122 | """ | |
123 | Gets branches for this repository |
|
123 | Gets branches for this repository | |
124 | Returns only not closed branches by default |
|
124 | Returns only not closed branches by default | |
125 |
|
125 | |||
126 | :param closed: return also closed branches for mercurial |
|
126 | :param closed: return also closed branches for mercurial | |
127 | :param normal: return also normal branches |
|
127 | :param normal: return also normal branches | |
128 | """ |
|
128 | """ | |
129 |
|
129 | |||
130 | if self._empty: |
|
130 | if self._empty: | |
131 | return {} |
|
131 | return {} | |
132 |
|
132 | |||
133 | bt = OrderedDict() |
|
133 | bt = OrderedDict() | |
134 | for bn, _heads, node, isclosed in sorted(self._repo.branchmap().iterbranches()): |
|
134 | for bn, _heads, node, isclosed in sorted(self._repo.branchmap().iterbranches()): | |
135 | if isclosed: |
|
135 | if isclosed: | |
136 | if closed: |
|
136 | if closed: | |
137 | bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node)) |
|
137 | bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node)) | |
138 | else: |
|
138 | else: | |
139 | if normal: |
|
139 | if normal: | |
140 | bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node)) |
|
140 | bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node)) | |
141 | return bt |
|
141 | return bt | |
142 |
|
142 | |||
143 | @LazyProperty |
|
143 | @LazyProperty | |
144 | def tags(self): |
|
144 | def tags(self): | |
145 | """ |
|
145 | """ | |
146 | Gets tags for this repository |
|
146 | Gets tags for this repository | |
147 | """ |
|
147 | """ | |
148 | return self._get_tags() |
|
148 | return self._get_tags() | |
149 |
|
149 | |||
150 | def _get_tags(self): |
|
150 | def _get_tags(self): | |
151 | if self._empty: |
|
151 | if self._empty: | |
152 | return {} |
|
152 | return {} | |
153 |
|
153 | |||
154 | return OrderedDict(sorted( |
|
154 | return OrderedDict(sorted( | |
155 | ((safe_str(n), ascii_str(mercurial.node.hex(h))) for n, h in self._repo.tags().items()), |
|
155 | ((safe_str(n), ascii_str(mercurial.node.hex(h))) for n, h in self._repo.tags().items()), | |
156 | reverse=True, |
|
156 | reverse=True, | |
157 | key=lambda x: x[0], # sort by name |
|
157 | key=lambda x: x[0], # sort by name | |
158 | )) |
|
158 | )) | |
159 |
|
159 | |||
160 | def tag(self, name, user, revision=None, message=None, date=None, |
|
160 | def tag(self, name, user, revision=None, message=None, date=None, | |
161 | **kwargs): |
|
161 | **kwargs): | |
162 | """ |
|
162 | """ | |
163 | Creates and returns a tag for the given ``revision``. |
|
163 | Creates and returns a tag for the given ``revision``. | |
164 |
|
164 | |||
165 | :param name: name for new tag |
|
165 | :param name: name for new tag | |
166 | :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>" |
|
166 | :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>" | |
167 | :param revision: changeset id for which new tag would be created |
|
167 | :param revision: changeset id for which new tag would be created | |
168 | :param message: message of the tag's commit |
|
168 | :param message: message of the tag's commit | |
169 | :param date: date of tag's commit |
|
169 | :param date: date of tag's commit | |
170 |
|
170 | |||
171 | :raises TagAlreadyExistError: if tag with same name already exists |
|
171 | :raises TagAlreadyExistError: if tag with same name already exists | |
172 | """ |
|
172 | """ | |
173 | if name in self.tags: |
|
173 | if name in self.tags: | |
174 | raise TagAlreadyExistError("Tag %s already exists" % name) |
|
174 | raise TagAlreadyExistError("Tag %s already exists" % name) | |
175 | changeset = self.get_changeset(revision) |
|
175 | changeset = self.get_changeset(revision) | |
176 | local = kwargs.setdefault('local', False) |
|
176 | local = kwargs.setdefault('local', False) | |
177 |
|
177 | |||
178 | if message is None: |
|
178 | if message is None: | |
179 | message = "Added tag %s for changeset %s" % (name, |
|
179 | message = "Added tag %s for changeset %s" % (name, | |
180 | changeset.short_id) |
|
180 | changeset.short_id) | |
181 |
|
181 | |||
182 | if date is None: |
|
182 | if date is None: | |
183 | date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S')) |
|
183 | date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S')) | |
184 |
|
184 | |||
185 | try: |
|
185 | try: | |
186 | mercurial.tags.tag(self._repo, safe_bytes(name), changeset._ctx.node(), safe_bytes(message), local, safe_bytes(user), date) |
|
186 | mercurial.tags.tag(self._repo, safe_bytes(name), changeset._ctx.node(), safe_bytes(message), local, safe_bytes(user), date) | |
187 | except mercurial.error.Abort as e: |
|
187 | except mercurial.error.Abort as e: | |
188 | raise RepositoryError(e.args[0]) |
|
188 | raise RepositoryError(e.args[0]) | |
189 |
|
189 | |||
190 | # Reinitialize tags |
|
190 | # Reinitialize tags | |
191 | self.tags = self._get_tags() |
|
191 | self.tags = self._get_tags() | |
192 | tag_id = self.tags[name] |
|
192 | tag_id = self.tags[name] | |
193 |
|
193 | |||
194 | return self.get_changeset(revision=tag_id) |
|
194 | return self.get_changeset(revision=tag_id) | |
195 |
|
195 | |||
196 | def remove_tag(self, name, user, message=None, date=None): |
|
196 | def remove_tag(self, name, user, message=None, date=None): | |
197 | """ |
|
197 | """ | |
198 | Removes tag with the given ``name``. |
|
198 | Removes tag with the given ``name``. | |
199 |
|
199 | |||
200 | :param name: name of the tag to be removed |
|
200 | :param name: name of the tag to be removed | |
201 | :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>" |
|
201 | :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>" | |
202 | :param message: message of the tag's removal commit |
|
202 | :param message: message of the tag's removal commit | |
203 | :param date: date of tag's removal commit |
|
203 | :param date: date of tag's removal commit | |
204 |
|
204 | |||
205 | :raises TagDoesNotExistError: if tag with given name does not exists |
|
205 | :raises TagDoesNotExistError: if tag with given name does not exists | |
206 | """ |
|
206 | """ | |
207 | if name not in self.tags: |
|
207 | if name not in self.tags: | |
208 | raise TagDoesNotExistError("Tag %s does not exist" % name) |
|
208 | raise TagDoesNotExistError("Tag %s does not exist" % name) | |
209 | if message is None: |
|
209 | if message is None: | |
210 | message = "Removed tag %s" % name |
|
210 | message = "Removed tag %s" % name | |
211 | if date is None: |
|
211 | if date is None: | |
212 | date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S')) |
|
212 | date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S')) | |
213 | local = False |
|
213 | local = False | |
214 |
|
214 | |||
215 | try: |
|
215 | try: | |
216 | mercurial.tags.tag(self._repo, safe_bytes(name), mercurial.commands.nullid, safe_bytes(message), local, safe_bytes(user), date) |
|
216 | mercurial.tags.tag(self._repo, safe_bytes(name), mercurial.commands.nullid, safe_bytes(message), local, safe_bytes(user), date) | |
217 | self.tags = self._get_tags() |
|
217 | self.tags = self._get_tags() | |
218 | except mercurial.error.Abort as e: |
|
218 | except mercurial.error.Abort as e: | |
219 | raise RepositoryError(e.args[0]) |
|
219 | raise RepositoryError(e.args[0]) | |
220 |
|
220 | |||
221 | @LazyProperty |
|
221 | @LazyProperty | |
222 | def bookmarks(self): |
|
222 | def bookmarks(self): | |
223 | """ |
|
223 | """ | |
224 | Gets bookmarks for this repository |
|
224 | Gets bookmarks for this repository | |
225 | """ |
|
225 | """ | |
226 | return self._get_bookmarks() |
|
226 | return self._get_bookmarks() | |
227 |
|
227 | |||
228 | def _get_bookmarks(self): |
|
228 | def _get_bookmarks(self): | |
229 | if self._empty: |
|
229 | if self._empty: | |
230 | return {} |
|
230 | return {} | |
231 |
|
231 | |||
232 | return OrderedDict(sorted( |
|
232 | return OrderedDict(sorted( | |
233 | ((safe_str(n), ascii_str(h)) for n, h in self._repo._bookmarks.items()), |
|
233 | ((safe_str(n), ascii_str(h)) for n, h in self._repo._bookmarks.items()), | |
234 | reverse=True, |
|
234 | reverse=True, | |
235 | key=lambda x: x[0], # sort by name |
|
235 | key=lambda x: x[0], # sort by name | |
236 | )) |
|
236 | )) | |
237 |
|
237 | |||
238 | def _get_all_revisions(self): |
|
238 | def _get_all_revisions(self): | |
239 | return [ascii_str(self._repo[x].hex()) for x in self._repo.filtered(b'visible').changelog.revs()] |
|
239 | return [ascii_str(self._repo[x].hex()) for x in self._repo.filtered(b'visible').changelog.revs()] | |
240 |
|
240 | |||
241 | def get_diff(self, rev1, rev2, path='', ignore_whitespace=False, |
|
241 | def get_diff(self, rev1, rev2, path='', ignore_whitespace=False, | |
242 | context=3): |
|
242 | context=3): | |
243 | """ |
|
243 | """ | |
244 | Returns (git like) *diff*, as plain text. Shows changes introduced by |
|
244 | Returns (git like) *diff*, as plain text. Shows changes introduced by | |
245 | ``rev2`` since ``rev1``. |
|
245 | ``rev2`` since ``rev1``. | |
246 |
|
246 | |||
247 | :param rev1: Entry point from which diff is shown. Can be |
|
247 | :param rev1: Entry point from which diff is shown. Can be | |
248 | ``self.EMPTY_CHANGESET`` - in this case, patch showing all |
|
248 | ``self.EMPTY_CHANGESET`` - in this case, patch showing all | |
249 | the changes since empty state of the repository until ``rev2`` |
|
249 | the changes since empty state of the repository until ``rev2`` | |
250 | :param rev2: Until which revision changes should be shown. |
|
250 | :param rev2: Until which revision changes should be shown. | |
251 | :param ignore_whitespace: If set to ``True``, would not show whitespace |
|
251 | :param ignore_whitespace: If set to ``True``, would not show whitespace | |
252 | changes. Defaults to ``False``. |
|
252 | changes. Defaults to ``False``. | |
253 | :param context: How many lines before/after changed lines should be |
|
253 | :param context: How many lines before/after changed lines should be | |
254 | shown. Defaults to ``3``. If negative value is passed-in, it will be |
|
254 | shown. Defaults to ``3``. If negative value is passed-in, it will be | |
255 | set to ``0`` instead. |
|
255 | set to ``0`` instead. | |
256 | """ |
|
256 | """ | |
257 |
|
257 | |||
258 | # Negative context values make no sense, and will result in |
|
258 | # Negative context values make no sense, and will result in | |
259 | # errors. Ensure this does not happen. |
|
259 | # errors. Ensure this does not happen. | |
260 | if context < 0: |
|
260 | if context < 0: | |
261 | context = 0 |
|
261 | context = 0 | |
262 |
|
262 | |||
263 | if hasattr(rev1, 'raw_id'): |
|
263 | if hasattr(rev1, 'raw_id'): | |
264 | rev1 = getattr(rev1, 'raw_id') |
|
264 | rev1 = getattr(rev1, 'raw_id') | |
265 |
|
265 | |||
266 | if hasattr(rev2, 'raw_id'): |
|
266 | if hasattr(rev2, 'raw_id'): | |
267 | rev2 = getattr(rev2, 'raw_id') |
|
267 | rev2 = getattr(rev2, 'raw_id') | |
268 |
|
268 | |||
269 | # Check if given revisions are present at repository (may raise |
|
269 | # Check if given revisions are present at repository (may raise | |
270 | # ChangesetDoesNotExistError) |
|
270 | # ChangesetDoesNotExistError) | |
271 | if rev1 != self.EMPTY_CHANGESET: |
|
271 | if rev1 != self.EMPTY_CHANGESET: | |
272 | self.get_changeset(rev1) |
|
272 | self.get_changeset(rev1) | |
273 | self.get_changeset(rev2) |
|
273 | self.get_changeset(rev2) | |
274 | if path: |
|
274 | if path: | |
275 | file_filter = mercurial.match.exact(path) |
|
275 | file_filter = mercurial.match.exact(path) | |
276 | else: |
|
276 | else: | |
277 | file_filter = None |
|
277 | file_filter = None | |
278 |
|
278 | |||
279 | return b''.join(mercurial.patch.diff(self._repo, rev1, rev2, match=file_filter, |
|
279 | return b''.join(mercurial.patch.diff(self._repo, rev1, rev2, match=file_filter, | |
280 | opts=mercurial.mdiff.diffopts(git=True, |
|
280 | opts=mercurial.mdiff.diffopts(git=True, | |
281 | showfunc=True, |
|
281 | showfunc=True, | |
282 | ignorews=ignore_whitespace, |
|
282 | ignorews=ignore_whitespace, | |
283 | context=context))) |
|
283 | context=context))) | |
284 |
|
284 | |||
285 | @classmethod |
|
285 | @classmethod | |
286 | def _check_url(cls, url, repoui=None): |
|
286 | def _check_url(cls, url, repoui=None): | |
287 | """ |
|
287 | """ | |
288 | Function will check given url and try to verify if it's a valid |
|
288 | Function will check given url and try to verify if it's a valid | |
289 | link. Sometimes it may happened that mercurial will issue basic |
|
289 | link. Sometimes it may happened that mercurial will issue basic | |
290 | auth request that can cause whole API to hang when used from python |
|
290 | auth request that can cause whole API to hang when used from python | |
291 | or other external calls. |
|
291 | or other external calls. | |
292 |
|
292 | |||
293 | On failures it'll raise urllib2.HTTPError, exception is also thrown |
|
293 | On failures it'll raise urllib2.HTTPError, exception is also thrown | |
294 | when the return code is non 200 |
|
294 | when the return code is non 200 | |
295 | """ |
|
295 | """ | |
296 | # check first if it's not an local url |
|
296 | # check first if it's not an local url | |
297 | if os.path.isdir(url) or url.startswith(b'file:'): |
|
297 | if os.path.isdir(url) or url.startswith(b'file:'): | |
298 | return True |
|
298 | return True | |
299 |
|
299 | |||
300 | if url.startswith(b'ssh:'): |
|
300 | if url.startswith(b'ssh:'): | |
301 | # in case of invalid uri or authentication issues, sshpeer will |
|
301 | # in case of invalid uri or authentication issues, sshpeer will | |
302 | # throw an exception. |
|
302 | # throw an exception. | |
303 | mercurial.sshpeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip') |
|
303 | mercurial.sshpeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip') | |
304 | return True |
|
304 | return True | |
305 |
|
305 | |||
306 | url_prefix = None |
|
306 | url_prefix = None | |
307 | if b'+' in url[:url.find(b'://')]: |
|
307 | if b'+' in url[:url.find(b'://')]: | |
308 | url_prefix, url = url.split(b'+', 1) |
|
308 | url_prefix, url = url.split(b'+', 1) | |
309 |
|
309 | |||
310 | handlers = [] |
|
310 | handlers = [] | |
311 | url_obj = mercurial.util.url(url) |
|
311 | url_obj = mercurial.util.url(url) | |
312 | test_uri, authinfo = url_obj.authinfo() |
|
312 | test_uri, authinfo = url_obj.authinfo() | |
313 | url_obj.passwd = b'*****' |
|
313 | url_obj.passwd = b'*****' | |
314 | cleaned_uri = str(url_obj) |
|
314 | cleaned_uri = str(url_obj) | |
315 |
|
315 | |||
316 | if authinfo: |
|
316 | if authinfo: | |
317 | # create a password manager |
|
317 | # create a password manager | |
318 | passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() |
|
318 | passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() | |
319 | passmgr.add_password(*authinfo) |
|
319 | passmgr.add_password(*authinfo) | |
320 |
|
320 | |||
321 | handlers.extend((mercurial.url.httpbasicauthhandler(passmgr), |
|
321 | handlers.extend((mercurial.url.httpbasicauthhandler(passmgr), | |
322 | mercurial.url.httpdigestauthhandler(passmgr))) |
|
322 | mercurial.url.httpdigestauthhandler(passmgr))) | |
323 |
|
323 | |||
324 | o = urllib.request.build_opener(*handlers) |
|
324 | o = urllib.request.build_opener(*handlers) | |
325 | o.addheaders = [('Content-Type', 'application/mercurial-0.1'), |
|
325 | o.addheaders = [('Content-Type', 'application/mercurial-0.1'), | |
326 | ('Accept', 'application/mercurial-0.1')] |
|
326 | ('Accept', 'application/mercurial-0.1')] | |
327 |
|
327 | |||
328 | req = urllib.request.Request( |
|
328 | req = urllib.request.Request( | |
329 | "%s?%s" % ( |
|
329 | "%s?%s" % ( | |
330 | test_uri, |
|
330 | test_uri, | |
331 | urllib.parse.urlencode({ |
|
331 | urllib.parse.urlencode({ | |
332 | 'cmd': 'between', |
|
332 | 'cmd': 'between', | |
333 | 'pairs': "%s-%s" % ('0' * 40, '0' * 40), |
|
333 | 'pairs': "%s-%s" % ('0' * 40, '0' * 40), | |
334 | }) |
|
334 | }) | |
335 | )) |
|
335 | )) | |
336 |
|
336 | |||
337 | try: |
|
337 | try: | |
338 | resp = o.open(req) |
|
338 | resp = o.open(req) | |
339 | if resp.code != 200: |
|
339 | if resp.code != 200: | |
340 | raise Exception('Return Code is not 200') |
|
340 | raise Exception('Return Code is not 200') | |
341 | except Exception as e: |
|
341 | except Exception as e: | |
342 | # means it cannot be cloned |
|
342 | # means it cannot be cloned | |
343 | raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e)) |
|
343 | raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e)) | |
344 |
|
344 | |||
345 | if not url_prefix: # skip svn+http://... (and git+... too) |
|
345 | if not url_prefix: # skip svn+http://... (and git+... too) | |
346 | # now check if it's a proper hg repo |
|
346 | # now check if it's a proper hg repo | |
347 | try: |
|
347 | try: | |
348 | mercurial.httppeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip') |
|
348 | mercurial.httppeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip') | |
349 | except Exception as e: |
|
349 | except Exception as e: | |
350 | raise urllib.error.URLError( |
|
350 | raise urllib.error.URLError( | |
351 | "url [%s] does not look like an hg repo org_exc: %s" |
|
351 | "url [%s] does not look like an hg repo org_exc: %s" | |
352 | % (cleaned_uri, e)) |
|
352 | % (cleaned_uri, e)) | |
353 |
|
353 | |||
354 | return True |
|
354 | return True | |
355 |
|
355 | |||
356 | def _get_repo(self, create, src_url=None, update_after_clone=False): |
|
356 | def _get_repo(self, create, src_url=None, update_after_clone=False): | |
357 | """ |
|
357 | """ | |
358 | Function will check for mercurial repository in given path and return |
|
358 | Function will check for mercurial repository in given path and return | |
359 | a localrepo object. If there is no repository in that path it will |
|
359 | a localrepo object. If there is no repository in that path it will | |
360 | raise an exception unless ``create`` parameter is set to True - in |
|
360 | raise an exception unless ``create`` parameter is set to True - in | |
361 | that case repository would be created and returned. |
|
361 | that case repository would be created and returned. | |
362 | If ``src_url`` is given, would try to clone repository from the |
|
362 | If ``src_url`` is given, would try to clone repository from the | |
363 | location at given clone_point. Additionally it'll make update to |
|
363 | location at given clone_point. Additionally it'll make update to | |
364 | working copy accordingly to ``update_after_clone`` flag |
|
364 | working copy accordingly to ``update_after_clone`` flag | |
365 | """ |
|
365 | """ | |
366 | try: |
|
366 | try: | |
367 | if src_url: |
|
367 | if src_url: | |
368 | url = safe_bytes(self._get_url(src_url)) |
|
368 | url = safe_bytes(self._get_url(src_url)) | |
369 | opts = {} |
|
369 | opts = {} | |
370 | if not update_after_clone: |
|
370 | if not update_after_clone: | |
371 | opts.update({'noupdate': True}) |
|
371 | opts.update({'noupdate': True}) | |
372 | MercurialRepository._check_url(url, self.baseui) |
|
372 | MercurialRepository._check_url(url, self.baseui) | |
373 | mercurial.commands.clone(self.baseui, url, safe_bytes(self.path), **opts) |
|
373 | mercurial.commands.clone(self.baseui, url, safe_bytes(self.path), **opts) | |
374 |
|
374 | |||
375 | # Don't try to create if we've already cloned repo |
|
375 | # Don't try to create if we've already cloned repo | |
376 | create = False |
|
376 | create = False | |
377 | return mercurial.localrepo.instance(self.baseui, safe_bytes(self.path), create=create) |
|
377 | return mercurial.localrepo.instance(self.baseui, safe_bytes(self.path), create=create) | |
378 | except (mercurial.error.Abort, mercurial.error.RepoError) as err: |
|
378 | except (mercurial.error.Abort, mercurial.error.RepoError) as err: | |
379 | if create: |
|
379 | if create: | |
380 | msg = "Cannot create repository at %s. Original error was %s" \ |
|
380 | msg = "Cannot create repository at %s. Original error was %s" \ | |
381 | % (self.name, err) |
|
381 | % (self.name, err) | |
382 | else: |
|
382 | else: | |
383 | msg = "Not valid repository at %s. Original error was %s" \ |
|
383 | msg = "Not valid repository at %s. Original error was %s" \ | |
384 | % (self.name, err) |
|
384 | % (self.name, err) | |
385 | raise RepositoryError(msg) |
|
385 | raise RepositoryError(msg) | |
386 |
|
386 | |||
387 | @LazyProperty |
|
387 | @LazyProperty | |
388 | def in_memory_changeset(self): |
|
388 | def in_memory_changeset(self): | |
389 | return MercurialInMemoryChangeset(self) |
|
389 | return MercurialInMemoryChangeset(self) | |
390 |
|
390 | |||
391 | @LazyProperty |
|
391 | @LazyProperty | |
392 | def description(self): |
|
392 | def description(self): | |
393 | _desc = self._repo.ui.config(b'web', b'description', None, untrusted=True) |
|
393 | _desc = self._repo.ui.config(b'web', b'description', None, untrusted=True) | |
394 | return safe_str(_desc or b'unknown') |
|
394 | return safe_str(_desc or b'unknown') | |
395 |
|
395 | |||
396 | @LazyProperty |
|
396 | @LazyProperty | |
397 | def contact(self): |
|
397 | def contact(self): | |
398 | return safe_str(mercurial.hgweb.common.get_contact(self._repo.ui.config) |
|
398 | return safe_str(mercurial.hgweb.common.get_contact(self._repo.ui.config) | |
399 | or b'Unknown') |
|
399 | or b'Unknown') | |
400 |
|
400 | |||
401 | @LazyProperty |
|
401 | @LazyProperty | |
402 | def last_change(self): |
|
402 | def last_change(self): | |
403 | """ |
|
403 | """ | |
404 | Returns last change made on this repository as datetime object |
|
404 | Returns last change made on this repository as datetime object | |
405 | """ |
|
405 | """ | |
406 | return date_fromtimestamp(self._get_mtime(), makedate()[1]) |
|
406 | return date_fromtimestamp(self._get_mtime(), makedate()[1]) | |
407 |
|
407 | |||
408 | def _get_mtime(self): |
|
408 | def _get_mtime(self): | |
409 | try: |
|
409 | try: | |
410 | return time.mktime(self.get_changeset().date.timetuple()) |
|
410 | return time.mktime(self.get_changeset().date.timetuple()) | |
411 | except RepositoryError: |
|
411 | except RepositoryError: | |
412 | # fallback to filesystem |
|
412 | # fallback to filesystem | |
413 | cl_path = os.path.join(self.path, '.hg', "00changelog.i") |
|
413 | cl_path = os.path.join(self.path, '.hg', "00changelog.i") | |
414 | st_path = os.path.join(self.path, '.hg', "store") |
|
414 | st_path = os.path.join(self.path, '.hg', "store") | |
415 | if os.path.exists(cl_path): |
|
415 | if os.path.exists(cl_path): | |
416 | return os.stat(cl_path).st_mtime |
|
416 | return os.stat(cl_path).st_mtime | |
417 | else: |
|
417 | else: | |
418 | return os.stat(st_path).st_mtime |
|
418 | return os.stat(st_path).st_mtime | |
419 |
|
419 | |||
420 | def _get_revision(self, revision): |
|
420 | def _get_revision(self, revision): | |
421 | """ |
|
421 | """ | |
422 | Given any revision identifier, returns a 40 char string with revision hash. |
|
422 | Given any revision identifier, returns a 40 char string with revision hash. | |
423 |
|
423 | |||
424 | :param revision: str or int or None |
|
424 | :param revision: str or int or None | |
425 | """ |
|
425 | """ | |
426 | if self._empty: |
|
426 | if self._empty: | |
427 | raise EmptyRepositoryError("There are no changesets yet") |
|
427 | raise EmptyRepositoryError("There are no changesets yet") | |
428 |
|
428 | |||
429 | if revision in [-1, None]: |
|
429 | if revision in [-1, None]: | |
430 | revision = b'tip' |
|
430 | revision = b'tip' | |
431 | elif isinstance(revision, unicode): |
|
431 | elif isinstance(revision, unicode): | |
432 | revision = safe_bytes(revision) |
|
432 | revision = safe_bytes(revision) | |
433 |
|
433 | |||
434 | try: |
|
434 | try: | |
435 | if isinstance(revision, int): |
|
435 | if isinstance(revision, int): | |
436 | return ascii_str(self._repo[revision].hex()) |
|
436 | return ascii_str(self._repo[revision].hex()) | |
437 | return ascii_str(mercurial.scmutil.revsymbol(self._repo, revision).hex()) |
|
437 | return ascii_str(mercurial.scmutil.revsymbol(self._repo, revision).hex()) | |
438 | except (IndexError, ValueError, mercurial.error.RepoLookupError, TypeError): |
|
438 | except (IndexError, ValueError, mercurial.error.RepoLookupError, TypeError): | |
439 | msg = "Revision %r does not exist for %s" % (safe_str(revision), self.name) |
|
439 | msg = "Revision %r does not exist for %s" % (safe_str(revision), self.name) | |
440 | raise ChangesetDoesNotExistError(msg) |
|
440 | raise ChangesetDoesNotExistError(msg) | |
441 | except (LookupError, ): |
|
441 | except (LookupError, ): | |
442 | msg = "Ambiguous identifier `%s` for %s" % (safe_str(revision), self.name) |
|
442 | msg = "Ambiguous identifier `%s` for %s" % (safe_str(revision), self.name) | |
443 | raise ChangesetDoesNotExistError(msg) |
|
443 | raise ChangesetDoesNotExistError(msg) | |
444 |
|
444 | |||
445 | def get_ref_revision(self, ref_type, ref_name): |
|
445 | def get_ref_revision(self, ref_type, ref_name): | |
446 | """ |
|
446 | """ | |
447 | Returns revision number for the given reference. |
|
447 | Returns revision number for the given reference. | |
448 | """ |
|
448 | """ | |
449 | if ref_type == 'rev' and not ref_name.strip('0'): |
|
449 | if ref_type == 'rev' and not ref_name.strip('0'): | |
450 | return self.EMPTY_CHANGESET |
|
450 | return self.EMPTY_CHANGESET | |
451 | # lookup up the exact node id |
|
451 | # lookup up the exact node id | |
452 | _revset_predicates = { |
|
452 | _revset_predicates = { | |
453 | 'branch': 'branch', |
|
453 | 'branch': 'branch', | |
454 | 'book': 'bookmark', |
|
454 | 'book': 'bookmark', | |
455 | 'tag': 'tag', |
|
455 | 'tag': 'tag', | |
456 | 'rev': 'id', |
|
456 | 'rev': 'id', | |
457 | } |
|
457 | } | |
458 | # avoid expensive branch(x) iteration over whole repo |
|
458 | # avoid expensive branch(x) iteration over whole repo | |
459 | rev_spec = "%%s & %s(%%s)" % _revset_predicates[ref_type] |
|
459 | rev_spec = "%%s & %s(%%s)" % _revset_predicates[ref_type] | |
460 | try: |
|
460 | try: | |
461 | revs = self._repo.revs(rev_spec, ref_name, ref_name) |
|
461 | revs = self._repo.revs(rev_spec, ref_name, ref_name) | |
462 | except LookupError: |
|
462 | except LookupError: | |
463 | msg = "Ambiguous identifier %s:%s for %s" % (ref_type, ref_name, self.name) |
|
463 | msg = "Ambiguous identifier %s:%s for %s" % (ref_type, ref_name, self.name) | |
464 | raise ChangesetDoesNotExistError(msg) |
|
464 | raise ChangesetDoesNotExistError(msg) | |
465 | except mercurial.error.RepoLookupError: |
|
465 | except mercurial.error.RepoLookupError: | |
466 | msg = "Revision %s:%s does not exist for %s" % (ref_type, ref_name, self.name) |
|
466 | msg = "Revision %s:%s does not exist for %s" % (ref_type, ref_name, self.name) | |
467 | raise ChangesetDoesNotExistError(msg) |
|
467 | raise ChangesetDoesNotExistError(msg) | |
468 | if revs: |
|
468 | if revs: | |
469 | revision = revs.last() |
|
469 | revision = revs.last() | |
470 | else: |
|
470 | else: | |
471 | # TODO: just report 'not found'? |
|
471 | # TODO: just report 'not found'? | |
472 | revision = ref_name |
|
472 | revision = ref_name | |
473 |
|
473 | |||
474 | return self._get_revision(revision) |
|
474 | return self._get_revision(revision) | |
475 |
|
475 | |||
476 | def _get_archives(self, archive_name='tip'): |
|
476 | def _get_archives(self, archive_name='tip'): | |
477 | allowed = self.baseui.configlist(b"web", b"allow_archive", |
|
477 | allowed = self.baseui.configlist(b"web", b"allow_archive", | |
478 | untrusted=True) |
|
478 | untrusted=True) | |
479 | for name, ext in [(b'zip', '.zip'), (b'gz', '.tar.gz'), (b'bz2', '.tar.bz2')]: |
|
479 | for name, ext in [(b'zip', '.zip'), (b'gz', '.tar.gz'), (b'bz2', '.tar.bz2')]: | |
480 | if name in allowed or self._repo.ui.configbool(b"web", |
|
480 | if name in allowed or self._repo.ui.configbool(b"web", | |
481 | b"allow" + name, |
|
481 | b"allow" + name, | |
482 | untrusted=True): |
|
482 | untrusted=True): | |
483 | yield {"type": name, "extension": ext, "node": archive_name} |
|
483 | yield {"type": safe_str(name), "extension": ext, "node": archive_name} | |
484 |
|
484 | |||
485 | def _get_url(self, url): |
|
485 | def _get_url(self, url): | |
486 | """ |
|
486 | """ | |
487 | Returns normalized url. If schema is not given, fall back to |
|
487 | Returns normalized url. If schema is not given, fall back to | |
488 | filesystem (``file:///``) schema. |
|
488 | filesystem (``file:///``) schema. | |
489 | """ |
|
489 | """ | |
490 | if url != 'default' and '://' not in url: |
|
490 | if url != 'default' and '://' not in url: | |
491 | url = "file:" + urllib.request.pathname2url(url) |
|
491 | url = "file:" + urllib.request.pathname2url(url) | |
492 | return url |
|
492 | return url | |
493 |
|
493 | |||
494 | def get_changeset(self, revision=None): |
|
494 | def get_changeset(self, revision=None): | |
495 | """ |
|
495 | """ | |
496 | Returns ``MercurialChangeset`` object representing repository's |
|
496 | Returns ``MercurialChangeset`` object representing repository's | |
497 | changeset at the given ``revision``. |
|
497 | changeset at the given ``revision``. | |
498 | """ |
|
498 | """ | |
499 | return MercurialChangeset(repository=self, revision=self._get_revision(revision)) |
|
499 | return MercurialChangeset(repository=self, revision=self._get_revision(revision)) | |
500 |
|
500 | |||
501 | def get_changesets(self, start=None, end=None, start_date=None, |
|
501 | def get_changesets(self, start=None, end=None, start_date=None, | |
502 | end_date=None, branch_name=None, reverse=False, max_revisions=None): |
|
502 | end_date=None, branch_name=None, reverse=False, max_revisions=None): | |
503 | """ |
|
503 | """ | |
504 | Returns iterator of ``MercurialChangeset`` objects from start to end |
|
504 | Returns iterator of ``MercurialChangeset`` objects from start to end | |
505 | (both are inclusive) |
|
505 | (both are inclusive) | |
506 |
|
506 | |||
507 | :param start: None, str, int or mercurial lookup format |
|
507 | :param start: None, str, int or mercurial lookup format | |
508 | :param end: None, str, int or mercurial lookup format |
|
508 | :param end: None, str, int or mercurial lookup format | |
509 | :param start_date: |
|
509 | :param start_date: | |
510 | :param end_date: |
|
510 | :param end_date: | |
511 | :param branch_name: |
|
511 | :param branch_name: | |
512 | :param reversed: return changesets in reversed order |
|
512 | :param reversed: return changesets in reversed order | |
513 | """ |
|
513 | """ | |
514 | start_raw_id = self._get_revision(start) |
|
514 | start_raw_id = self._get_revision(start) | |
515 | start_pos = None if start is None else self.revisions.index(start_raw_id) |
|
515 | start_pos = None if start is None else self.revisions.index(start_raw_id) | |
516 | end_raw_id = self._get_revision(end) |
|
516 | end_raw_id = self._get_revision(end) | |
517 | end_pos = None if end is None else self.revisions.index(end_raw_id) |
|
517 | end_pos = None if end is None else self.revisions.index(end_raw_id) | |
518 |
|
518 | |||
519 | if start_pos is not None and end_pos is not None and start_pos > end_pos: |
|
519 | if start_pos is not None and end_pos is not None and start_pos > end_pos: | |
520 | raise RepositoryError("Start revision '%s' cannot be " |
|
520 | raise RepositoryError("Start revision '%s' cannot be " | |
521 | "after end revision '%s'" % (start, end)) |
|
521 | "after end revision '%s'" % (start, end)) | |
522 |
|
522 | |||
523 | if branch_name and branch_name not in self.allbranches: |
|
523 | if branch_name and branch_name not in self.allbranches: | |
524 | msg = "Branch %r not found in %s" % (branch_name, self.name) |
|
524 | msg = "Branch %r not found in %s" % (branch_name, self.name) | |
525 | raise BranchDoesNotExistError(msg) |
|
525 | raise BranchDoesNotExistError(msg) | |
526 | if end_pos is not None: |
|
526 | if end_pos is not None: | |
527 | end_pos += 1 |
|
527 | end_pos += 1 | |
528 | # filter branches |
|
528 | # filter branches | |
529 | filter_ = [] |
|
529 | filter_ = [] | |
530 | if branch_name: |
|
530 | if branch_name: | |
531 | filter_.append(b'branch("%s")' % safe_bytes(branch_name)) |
|
531 | filter_.append(b'branch("%s")' % safe_bytes(branch_name)) | |
532 | if start_date: |
|
532 | if start_date: | |
533 | filter_.append(b'date(">%s")' % safe_bytes(str(start_date))) |
|
533 | filter_.append(b'date(">%s")' % safe_bytes(str(start_date))) | |
534 | if end_date: |
|
534 | if end_date: | |
535 | filter_.append(b'date("<%s")' % safe_bytes(str(end_date))) |
|
535 | filter_.append(b'date("<%s")' % safe_bytes(str(end_date))) | |
536 | if filter_ or max_revisions: |
|
536 | if filter_ or max_revisions: | |
537 | if filter_: |
|
537 | if filter_: | |
538 | revspec = b' and '.join(filter_) |
|
538 | revspec = b' and '.join(filter_) | |
539 | else: |
|
539 | else: | |
540 | revspec = b'all()' |
|
540 | revspec = b'all()' | |
541 | if max_revisions: |
|
541 | if max_revisions: | |
542 | revspec = b'limit(%s, %d)' % (revspec, max_revisions) |
|
542 | revspec = b'limit(%s, %d)' % (revspec, max_revisions) | |
543 | revisions = mercurial.scmutil.revrange(self._repo, [revspec]) |
|
543 | revisions = mercurial.scmutil.revrange(self._repo, [revspec]) | |
544 | else: |
|
544 | else: | |
545 | revisions = self.revisions |
|
545 | revisions = self.revisions | |
546 |
|
546 | |||
547 | # this is very much a hack to turn this into a list; a better solution |
|
547 | # this is very much a hack to turn this into a list; a better solution | |
548 | # would be to get rid of this function entirely and use revsets |
|
548 | # would be to get rid of this function entirely and use revsets | |
549 | revs = list(revisions)[start_pos:end_pos] |
|
549 | revs = list(revisions)[start_pos:end_pos] | |
550 | if reverse: |
|
550 | if reverse: | |
551 | revs.reverse() |
|
551 | revs.reverse() | |
552 |
|
552 | |||
553 | return CollectionGenerator(self, revs) |
|
553 | return CollectionGenerator(self, revs) | |
554 |
|
554 | |||
555 | def pull(self, url): |
|
555 | def pull(self, url): | |
556 | """ |
|
556 | """ | |
557 | Tries to pull changes from external location. |
|
557 | Tries to pull changes from external location. | |
558 | """ |
|
558 | """ | |
559 | other = mercurial.hg.peer(self._repo, {}, safe_bytes(self._get_url(url))) |
|
559 | other = mercurial.hg.peer(self._repo, {}, safe_bytes(self._get_url(url))) | |
560 | try: |
|
560 | try: | |
561 | mercurial.exchange.pull(self._repo, other, heads=None, force=None) |
|
561 | mercurial.exchange.pull(self._repo, other, heads=None, force=None) | |
562 | except mercurial.error.Abort as err: |
|
562 | except mercurial.error.Abort as err: | |
563 | # Propagate error but with vcs's type |
|
563 | # Propagate error but with vcs's type | |
564 | raise RepositoryError(str(err)) |
|
564 | raise RepositoryError(str(err)) | |
565 |
|
565 | |||
566 | @LazyProperty |
|
566 | @LazyProperty | |
567 | def workdir(self): |
|
567 | def workdir(self): | |
568 | """ |
|
568 | """ | |
569 | Returns ``Workdir`` instance for this repository. |
|
569 | Returns ``Workdir`` instance for this repository. | |
570 | """ |
|
570 | """ | |
571 | return MercurialWorkdir(self) |
|
571 | return MercurialWorkdir(self) | |
572 |
|
572 | |||
573 | def get_config_value(self, section, name=None, config_file=None): |
|
573 | def get_config_value(self, section, name=None, config_file=None): | |
574 | """ |
|
574 | """ | |
575 | Returns configuration value for a given [``section``] and ``name``. |
|
575 | Returns configuration value for a given [``section``] and ``name``. | |
576 |
|
576 | |||
577 | :param section: Section we want to retrieve value from |
|
577 | :param section: Section we want to retrieve value from | |
578 | :param name: Name of configuration we want to retrieve |
|
578 | :param name: Name of configuration we want to retrieve | |
579 | :param config_file: A path to file which should be used to retrieve |
|
579 | :param config_file: A path to file which should be used to retrieve | |
580 | configuration from (might also be a list of file paths) |
|
580 | configuration from (might also be a list of file paths) | |
581 | """ |
|
581 | """ | |
582 | if config_file is None: |
|
582 | if config_file is None: | |
583 | config_file = [] |
|
583 | config_file = [] | |
584 | elif isinstance(config_file, str): |
|
584 | elif isinstance(config_file, str): | |
585 | config_file = [config_file] |
|
585 | config_file = [config_file] | |
586 |
|
586 | |||
587 | config = self._repo.ui |
|
587 | config = self._repo.ui | |
588 | if config_file: |
|
588 | if config_file: | |
589 | config = mercurial.ui.ui() |
|
589 | config = mercurial.ui.ui() | |
590 | for path in config_file: |
|
590 | for path in config_file: | |
591 | config.readconfig(safe_bytes(path)) |
|
591 | config.readconfig(safe_bytes(path)) | |
592 |
|
|
592 | value = config.config(safe_bytes(section), safe_bytes(name)) | |
|
593 | return value if value is None else safe_str(value) | |||
593 |
|
594 | |||
594 | def get_user_name(self, config_file=None): |
|
595 | def get_user_name(self, config_file=None): | |
595 | """ |
|
596 | """ | |
596 | Returns user's name from global configuration file. |
|
597 | Returns user's name from global configuration file. | |
597 |
|
598 | |||
598 | :param config_file: A path to file which should be used to retrieve |
|
599 | :param config_file: A path to file which should be used to retrieve | |
599 | configuration from (might also be a list of file paths) |
|
600 | configuration from (might also be a list of file paths) | |
600 | """ |
|
601 | """ | |
601 | username = self.get_config_value('ui', 'username', config_file=config_file) |
|
602 | username = self.get_config_value('ui', 'username', config_file=config_file) | |
602 | if username: |
|
603 | if username: | |
603 | return author_name(username) |
|
604 | return author_name(username) | |
604 | return None |
|
605 | return None | |
605 |
|
606 | |||
606 | def get_user_email(self, config_file=None): |
|
607 | def get_user_email(self, config_file=None): | |
607 | """ |
|
608 | """ | |
608 | Returns user's email from global configuration file. |
|
609 | Returns user's email from global configuration file. | |
609 |
|
610 | |||
610 | :param config_file: A path to file which should be used to retrieve |
|
611 | :param config_file: A path to file which should be used to retrieve | |
611 | configuration from (might also be a list of file paths) |
|
612 | configuration from (might also be a list of file paths) | |
612 | """ |
|
613 | """ | |
613 | username = self.get_config_value('ui', 'username', config_file=config_file) |
|
614 | username = self.get_config_value('ui', 'username', config_file=config_file) | |
614 | if username: |
|
615 | if username: | |
615 | return author_email(username) |
|
616 | return author_email(username) | |
616 | return None |
|
617 | return None |
@@ -1,24 +1,24 b'' | |||||
1 | import mercurial.merge |
|
1 | import mercurial.merge | |
2 |
|
2 | |||
3 | from kallithea.lib.vcs.backends.base import BaseWorkdir |
|
3 | from kallithea.lib.vcs.backends.base import BaseWorkdir | |
4 | from kallithea.lib.vcs.exceptions import BranchDoesNotExistError |
|
4 | from kallithea.lib.vcs.exceptions import BranchDoesNotExistError | |
5 | from kallithea.lib.vcs.utils import ascii_bytes, ascii_str |
|
5 | from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, safe_str | |
6 |
|
6 | |||
7 |
|
7 | |||
8 | class MercurialWorkdir(BaseWorkdir): |
|
8 | class MercurialWorkdir(BaseWorkdir): | |
9 |
|
9 | |||
10 | def get_branch(self): |
|
10 | def get_branch(self): | |
11 | return self.repository._repo.dirstate.branch() |
|
11 | return safe_str(self.repository._repo.dirstate.branch()) | |
12 |
|
12 | |||
13 | def get_changeset(self): |
|
13 | def get_changeset(self): | |
14 | wk_dir_id = ascii_str(self.repository._repo[None].parents()[0].hex()) |
|
14 | wk_dir_id = ascii_str(self.repository._repo[None].parents()[0].hex()) | |
15 | return self.repository.get_changeset(wk_dir_id) |
|
15 | return self.repository.get_changeset(wk_dir_id) | |
16 |
|
16 | |||
17 | def checkout_branch(self, branch=None): |
|
17 | def checkout_branch(self, branch=None): | |
18 | if branch is None: |
|
18 | if branch is None: | |
19 | branch = self.repository.DEFAULT_BRANCH_NAME |
|
19 | branch = self.repository.DEFAULT_BRANCH_NAME | |
20 | if branch not in self.repository.branches: |
|
20 | if branch not in self.repository.branches: | |
21 | raise BranchDoesNotExistError |
|
21 | raise BranchDoesNotExistError | |
22 |
|
22 | |||
23 | raw_id = self.repository.branches[branch] |
|
23 | raw_id = self.repository.branches[branch] | |
24 | mercurial.merge.update(self.repository._repo, ascii_bytes(raw_id), False, False, None) |
|
24 | mercurial.merge.update(self.repository._repo, ascii_bytes(raw_id), False, False, None) |
General Comments 0
You need to be logged in to leave comments.
Login now