Show More
@@ -0,0 +1,24 b'' | |||
|
1 | #!/usr/bin/env python3 | |
|
2 | ||
|
3 | # hg files 'set:!binary()&grep("^#!.*python")' 'set:**.py' | xargs scripts/source_format.py | |
|
4 | ||
|
5 | import re | |
|
6 | import sys | |
|
7 | ||
|
8 | ||
|
9 | filenames = sys.argv[1:] | |
|
10 | ||
|
11 | for fn in filenames: | |
|
12 | with open(fn) as f: | |
|
13 | org_content = f.read() | |
|
14 | ||
|
15 | mod_name = fn[:-3] if fn.endswith('.py') else fn | |
|
16 | mod_name = mod_name[:-9] if mod_name.endswith('/__init__') else mod_name | |
|
17 | mod_name = mod_name.replace('/', '.') | |
|
18 | def f(m): | |
|
19 | return '"""\n%s\n%s\n' % (mod_name, '~' * len(mod_name)) | |
|
20 | new_content = re.sub(r'^"""\n(kallithea\..*\n)(~+\n)?', f, org_content, count=1, flags=re.MULTILINE) | |
|
21 | ||
|
22 | if new_content != org_content: | |
|
23 | with open(fn, 'w') as f: | |
|
24 | f.write(new_content) |
@@ -1,191 +1,191 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | # This program is free software: you can redistribute it and/or modify |
|
3 | 3 | # it under the terms of the GNU General Public License as published by |
|
4 | 4 | # the Free Software Foundation, either version 3 of the License, or |
|
5 | 5 | # (at your option) any later version. |
|
6 | 6 | # |
|
7 | 7 | # This program is distributed in the hope that it will be useful, |
|
8 | 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
9 | 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
10 | 10 | # GNU General Public License for more details. |
|
11 | 11 | # |
|
12 | 12 | # You should have received a copy of the GNU General Public License |
|
13 | 13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
14 | 14 | """ |
|
15 | 15 | kallithea.bin.vcs_hooks |
|
16 | ~~~~~~~~~~~~~~~~~~~ | |
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~ | |
|
17 | 17 | |
|
18 | 18 | Entry points for Kallithea hooking into Mercurial and Git. |
|
19 | 19 | |
|
20 | 20 | This file was forked by the Kallithea project in July 2014. |
|
21 | 21 | Original author and date, and relevant copyright and licensing information is below: |
|
22 | 22 | :created_on: Aug 6, 2010 |
|
23 | 23 | :author: marcink |
|
24 | 24 | :copyright: (c) 2013 RhodeCode GmbH, and others. |
|
25 | 25 | :license: GPLv3, see LICENSE.md for more details. |
|
26 | 26 | """ |
|
27 | 27 | |
|
28 | 28 | import os |
|
29 | 29 | import sys |
|
30 | 30 | |
|
31 | 31 | import mercurial.scmutil |
|
32 | 32 | import paste.deploy |
|
33 | 33 | |
|
34 | 34 | import kallithea |
|
35 | 35 | import kallithea.config.application |
|
36 | 36 | from kallithea.lib import hooks, webutils |
|
37 | 37 | from kallithea.lib.utils2 import HookEnvironmentError, ascii_str, get_hook_environment, safe_bytes, safe_str |
|
38 | 38 | from kallithea.lib.vcs.backends.base import EmptyChangeset |
|
39 | 39 | from kallithea.lib.vcs.utils.helpers import get_scm_size |
|
40 | 40 | from kallithea.model import db |
|
41 | 41 | |
|
42 | 42 | |
|
43 | 43 | def repo_size(ui, repo, hooktype=None, **kwargs): |
|
44 | 44 | """Show size of Mercurial repository. |
|
45 | 45 | |
|
46 | 46 | Called as Mercurial hook changegroup.repo_size after push. |
|
47 | 47 | """ |
|
48 | 48 | size_hg, size_root = get_scm_size('.hg', safe_str(repo.root)) |
|
49 | 49 | |
|
50 | 50 | last_cs = repo[len(repo) - 1] |
|
51 | 51 | |
|
52 | 52 | msg = ('Repository size .hg: %s Checkout: %s Total: %s\n' |
|
53 | 53 | 'Last revision is now r%s:%s\n') % ( |
|
54 | 54 | webutils.format_byte_size(size_hg), |
|
55 | 55 | webutils.format_byte_size(size_root), |
|
56 | 56 | webutils.format_byte_size(size_hg + size_root), |
|
57 | 57 | last_cs.rev(), |
|
58 | 58 | ascii_str(last_cs.hex())[:12], |
|
59 | 59 | ) |
|
60 | 60 | ui.status(safe_bytes(msg)) |
|
61 | 61 | |
|
62 | 62 | |
|
63 | 63 | def pull_action(ui, repo, **kwargs): |
|
64 | 64 | """Logs user pull action |
|
65 | 65 | |
|
66 | 66 | Called as Mercurial hook outgoing.kallithea_pull_action. |
|
67 | 67 | """ |
|
68 | 68 | hooks.log_pull_action() |
|
69 | 69 | |
|
70 | 70 | |
|
71 | 71 | def push_action(ui, repo, node, node_last, **kwargs): |
|
72 | 72 | """ |
|
73 | 73 | Register that changes have been added to the repo - log the action *and* invalidate caches. |
|
74 | 74 | Note: This hook is not only logging, but also the side effect invalidating |
|
75 | 75 | caches! The function should perhaps be renamed. |
|
76 | 76 | |
|
77 | 77 | Called as Mercurial hook changegroup.kallithea_push_action . |
|
78 | 78 | |
|
79 | 79 | The pushed changesets is given by the revset 'node:node_last'. |
|
80 | 80 | """ |
|
81 | 81 | revs = [ascii_str(repo[r].hex()) for r in mercurial.scmutil.revrange(repo, [b'%s:%s' % (node, node_last)])] |
|
82 | 82 | hooks.process_pushed_raw_ids(revs) |
|
83 | 83 | |
|
84 | 84 | |
|
85 | 85 | def _git_hook_environment(repo_path): |
|
86 | 86 | """ |
|
87 | 87 | Create a light-weight environment for stand-alone scripts and return an UI and the |
|
88 | 88 | db repository. |
|
89 | 89 | |
|
90 | 90 | Git hooks are executed as subprocess of Git while Kallithea is waiting, and |
|
91 | 91 | they thus need enough info to be able to create an app environment and |
|
92 | 92 | connect to the database. |
|
93 | 93 | """ |
|
94 | 94 | extras = get_hook_environment() |
|
95 | 95 | |
|
96 | 96 | path_to_ini_file = extras['config'] |
|
97 | 97 | config = paste.deploy.appconfig('config:' + path_to_ini_file) |
|
98 | 98 | #logging.config.fileConfig(ini_file_path) # Note: we are in a different process - don't use configured logging |
|
99 | 99 | kallithea.config.application.make_app(config.global_conf, **config.local_conf) |
|
100 | 100 | |
|
101 | 101 | # fix if it's not a bare repo |
|
102 | 102 | if repo_path.endswith(os.sep + '.git'): |
|
103 | 103 | repo_path = repo_path[:-5] |
|
104 | 104 | |
|
105 | 105 | repo = db.Repository.get_by_full_path(repo_path) |
|
106 | 106 | if not repo: |
|
107 | 107 | raise OSError('Repository %s not found in database' % repo_path) |
|
108 | 108 | |
|
109 | 109 | return repo |
|
110 | 110 | |
|
111 | 111 | |
|
112 | 112 | def pre_receive(repo_path, git_stdin_lines): |
|
113 | 113 | """Called from Git pre-receive hook. |
|
114 | 114 | The returned value is used as hook exit code and must be 0. |
|
115 | 115 | """ |
|
116 | 116 | # Currently unused. TODO: remove? |
|
117 | 117 | return 0 |
|
118 | 118 | |
|
119 | 119 | |
|
120 | 120 | def post_receive(repo_path, git_stdin_lines): |
|
121 | 121 | """Called from Git post-receive hook. |
|
122 | 122 | The returned value is used as hook exit code and must be 0. |
|
123 | 123 | """ |
|
124 | 124 | try: |
|
125 | 125 | repo = _git_hook_environment(repo_path) |
|
126 | 126 | except HookEnvironmentError as e: |
|
127 | 127 | sys.stderr.write("Skipping Kallithea Git post-receive hook %r.\nGit was apparently not invoked by Kallithea: %s\n" % (sys.argv[0], e)) |
|
128 | 128 | return 0 |
|
129 | 129 | |
|
130 | 130 | # the post push hook should never use the cached instance |
|
131 | 131 | scm_repo = repo.scm_instance_no_cache() |
|
132 | 132 | |
|
133 | 133 | rev_data = [] |
|
134 | 134 | for l in git_stdin_lines: |
|
135 | 135 | old_rev, new_rev, ref = l.strip().split(' ') |
|
136 | 136 | _ref_data = ref.split('/') |
|
137 | 137 | if _ref_data[1] in ['tags', 'heads']: |
|
138 | 138 | rev_data.append({'old_rev': old_rev, |
|
139 | 139 | 'new_rev': new_rev, |
|
140 | 140 | 'ref': ref, |
|
141 | 141 | 'type': _ref_data[1], |
|
142 | 142 | 'name': '/'.join(_ref_data[2:])}) |
|
143 | 143 | |
|
144 | 144 | git_revs = [] |
|
145 | 145 | for push_ref in rev_data: |
|
146 | 146 | _type = push_ref['type'] |
|
147 | 147 | if _type == 'heads': |
|
148 | 148 | if push_ref['old_rev'] == EmptyChangeset().raw_id: |
|
149 | 149 | # update the symbolic ref if we push new repo |
|
150 | 150 | if scm_repo.is_empty(): |
|
151 | 151 | scm_repo._repo.refs.set_symbolic_ref( |
|
152 | 152 | b'HEAD', |
|
153 | 153 | b'refs/heads/%s' % safe_bytes(push_ref['name'])) |
|
154 | 154 | |
|
155 | 155 | # build exclude list without the ref |
|
156 | 156 | cmd = ['for-each-ref', '--format=%(refname)', 'refs/heads/*'] |
|
157 | 157 | stdout = scm_repo.run_git_command(cmd) |
|
158 | 158 | ref = push_ref['ref'] |
|
159 | 159 | heads = [head for head in stdout.splitlines() if head != ref] |
|
160 | 160 | # now list the git revs while excluding from the list |
|
161 | 161 | cmd = ['log', push_ref['new_rev'], '--reverse', '--pretty=format:%H'] |
|
162 | 162 | cmd.append('--not') |
|
163 | 163 | cmd.extend(heads) # empty list is ok |
|
164 | 164 | stdout = scm_repo.run_git_command(cmd) |
|
165 | 165 | git_revs += stdout.splitlines() |
|
166 | 166 | |
|
167 | 167 | elif push_ref['new_rev'] == EmptyChangeset().raw_id: |
|
168 | 168 | # delete branch case |
|
169 | 169 | git_revs += ['delete_branch=>%s' % push_ref['name']] |
|
170 | 170 | else: |
|
171 | 171 | cmd = ['log', '%(old_rev)s..%(new_rev)s' % push_ref, |
|
172 | 172 | '--reverse', '--pretty=format:%H'] |
|
173 | 173 | stdout = scm_repo.run_git_command(cmd) |
|
174 | 174 | git_revs += stdout.splitlines() |
|
175 | 175 | |
|
176 | 176 | elif _type == 'tags': |
|
177 | 177 | git_revs += ['tag=>%s' % push_ref['name']] |
|
178 | 178 | |
|
179 | 179 | hooks.process_pushed_raw_ids(git_revs) |
|
180 | 180 | |
|
181 | 181 | return 0 |
|
182 | 182 | |
|
183 | 183 | |
|
184 | 184 | # Almost exactly like Mercurial contrib/hg-ssh: |
|
185 | 185 | def rejectpush(ui, **kwargs): |
|
186 | 186 | """Mercurial hook to be installed as pretxnopen and prepushkey for read-only repos. |
|
187 | 187 | Return value 1 will make the hook fail and reject the push. |
|
188 | 188 | """ |
|
189 | 189 | ex = get_hook_environment() |
|
190 | 190 | ui.warn(safe_bytes("Push access to %r denied\n" % ex.repository)) |
|
191 | 191 | return 1 |
@@ -1,73 +1,73 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | # This program is free software: you can redistribute it and/or modify |
|
3 | 3 | # it under the terms of the GNU General Public License as published by |
|
4 | 4 | # the Free Software Foundation, either version 3 of the License, or |
|
5 | 5 | # (at your option) any later version. |
|
6 | 6 | # |
|
7 | 7 | # This program is distributed in the hope that it will be useful, |
|
8 | 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
9 | 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
10 | 10 | # GNU General Public License for more details. |
|
11 | 11 | # |
|
12 | 12 | # You should have received a copy of the GNU General Public License |
|
13 | 13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
14 | 14 | """ |
|
15 |
kallithea. |
|
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
|
15 | kallithea.config.middleware.https_fixup | |
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
|
17 | 17 | |
|
18 | 18 | middleware to handle https correctly |
|
19 | 19 | |
|
20 | 20 | This file was forked by the Kallithea project in July 2014. |
|
21 | 21 | Original author and date, and relevant copyright and licensing information is below: |
|
22 | 22 | :created_on: May 23, 2010 |
|
23 | 23 | :author: marcink |
|
24 | 24 | :copyright: (c) 2013 RhodeCode GmbH, and others. |
|
25 | 25 | :license: GPLv3, see LICENSE.md for more details. |
|
26 | 26 | """ |
|
27 | 27 | |
|
28 | 28 | |
|
29 | 29 | from kallithea.lib.utils2 import asbool |
|
30 | 30 | |
|
31 | 31 | |
|
32 | 32 | class HttpsFixup(object): |
|
33 | 33 | |
|
34 | 34 | def __init__(self, app, config): |
|
35 | 35 | self.application = app |
|
36 | 36 | self.config = config |
|
37 | 37 | |
|
38 | 38 | def __call__(self, environ, start_response): |
|
39 | 39 | self.__fixup(environ) |
|
40 | 40 | debug = asbool(self.config.get('debug')) |
|
41 | 41 | is_ssl = environ['wsgi.url_scheme'] == 'https' |
|
42 | 42 | |
|
43 | 43 | def custom_start_response(status, headers, exc_info=None): |
|
44 | 44 | if is_ssl and asbool(self.config.get('use_htsts')) and not debug: |
|
45 | 45 | headers.append(('Strict-Transport-Security', |
|
46 | 46 | 'max-age=8640000; includeSubDomains')) |
|
47 | 47 | return start_response(status, headers, exc_info) |
|
48 | 48 | |
|
49 | 49 | return self.application(environ, custom_start_response) |
|
50 | 50 | |
|
51 | 51 | def __fixup(self, environ): |
|
52 | 52 | """ |
|
53 | 53 | Function to fixup the environ as needed. In order to use this |
|
54 | 54 | middleware you should set this header inside your |
|
55 | 55 | proxy ie. nginx, apache etc. |
|
56 | 56 | """ |
|
57 | 57 | # DETECT PROTOCOL ! |
|
58 | 58 | if 'HTTP_X_URL_SCHEME' in environ: |
|
59 | 59 | proto = environ.get('HTTP_X_URL_SCHEME') |
|
60 | 60 | elif 'HTTP_X_FORWARDED_SCHEME' in environ: |
|
61 | 61 | proto = environ.get('HTTP_X_FORWARDED_SCHEME') |
|
62 | 62 | elif 'HTTP_X_FORWARDED_PROTO' in environ: |
|
63 | 63 | proto = environ.get('HTTP_X_FORWARDED_PROTO') |
|
64 | 64 | else: |
|
65 | 65 | proto = 'http' |
|
66 | 66 | org_proto = proto |
|
67 | 67 | |
|
68 | 68 | # if we have force, just override |
|
69 | 69 | if asbool(self.config.get('force_https')): |
|
70 | 70 | proto = 'https' |
|
71 | 71 | |
|
72 | 72 | environ['wsgi.url_scheme'] = proto |
|
73 | 73 | environ['wsgi._org_proto'] = org_proto |
@@ -1,41 +1,41 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | # This program is free software: you can redistribute it and/or modify |
|
3 | 3 | # it under the terms of the GNU General Public License as published by |
|
4 | 4 | # the Free Software Foundation, either version 3 of the License, or |
|
5 | 5 | # (at your option) any later version. |
|
6 | 6 | # |
|
7 | 7 | # This program is distributed in the hope that it will be useful, |
|
8 | 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
9 | 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
10 | 10 | # GNU General Public License for more details. |
|
11 | 11 | # |
|
12 | 12 | # You should have received a copy of the GNU General Public License |
|
13 | 13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
14 | 14 | """ |
|
15 |
kallithea. |
|
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
|
15 | kallithea.config.middleware.permanent_repo_url | |
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
|
17 | 17 | |
|
18 | 18 | middleware to handle permanent repo URLs, replacing PATH_INFO '/_123/yada' with |
|
19 | 19 | '/name/of/repo/yada' after looking 123 up in the database. |
|
20 | 20 | """ |
|
21 | 21 | |
|
22 | 22 | |
|
23 | 23 | from kallithea.lib.utils import fix_repo_id_name |
|
24 | 24 | from kallithea.lib.utils2 import safe_bytes, safe_str |
|
25 | 25 | |
|
26 | 26 | |
|
27 | 27 | class PermanentRepoUrl(object): |
|
28 | 28 | |
|
29 | 29 | def __init__(self, app, config): |
|
30 | 30 | self.application = app |
|
31 | 31 | self.config = config |
|
32 | 32 | |
|
33 | 33 | def __call__(self, environ, start_response): |
|
34 | 34 | # Extract path_info as get_path_info does, but do it explicitly because |
|
35 | 35 | # we also have to do the reverse operation when patching it back in |
|
36 | 36 | path_info = safe_str(environ['PATH_INFO'].encode('latin1')) |
|
37 | 37 | if path_info.startswith('/'): # it must |
|
38 | 38 | path_info = '/' + fix_repo_id_name(path_info[1:]) |
|
39 | 39 | environ['PATH_INFO'] = safe_bytes(path_info).decode('latin1') |
|
40 | 40 | |
|
41 | 41 | return self.application(environ, start_response) |
@@ -1,227 +1,227 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | # This program is free software: you can redistribute it and/or modify |
|
3 | 3 | # it under the terms of the GNU General Public License as published by |
|
4 | 4 | # the Free Software Foundation, either version 3 of the License, or |
|
5 | 5 | # (at your option) any later version. |
|
6 | 6 | # |
|
7 | 7 | # This program is distributed in the hope that it will be useful, |
|
8 | 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
9 | 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
10 | 10 | # GNU General Public License for more details. |
|
11 | 11 | # |
|
12 | 12 | # You should have received a copy of the GNU General Public License |
|
13 | 13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
14 | 14 | """ |
|
15 |
kallithea. |
|
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
|
15 | kallithea.config.middleware.pygrack | |
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
|
17 | 17 | |
|
18 | 18 | Python implementation of git-http-backend's Smart HTTP protocol |
|
19 | 19 | |
|
20 | 20 | Based on original code from git_http_backend.py project. |
|
21 | 21 | |
|
22 | 22 | Copyright (c) 2010 Daniel Dotsenko <dotsa@hotmail.com> |
|
23 | 23 | Copyright (c) 2012 Marcin Kuzminski <marcin@python-works.com> |
|
24 | 24 | |
|
25 | 25 | This file was forked by the Kallithea project in July 2014. |
|
26 | 26 | """ |
|
27 | 27 | |
|
28 | 28 | import logging |
|
29 | 29 | import os |
|
30 | 30 | import socket |
|
31 | 31 | import traceback |
|
32 | 32 | |
|
33 | 33 | from dulwich.server import update_server_info |
|
34 | 34 | from dulwich.web import GunzipFilter, LimitedInputFilter |
|
35 | 35 | from webob import Request, Response, exc |
|
36 | 36 | |
|
37 | 37 | import kallithea |
|
38 | 38 | from kallithea.lib.utils2 import ascii_bytes |
|
39 | 39 | from kallithea.lib.vcs import get_repo, subprocessio |
|
40 | 40 | |
|
41 | 41 | |
|
42 | 42 | log = logging.getLogger(__name__) |
|
43 | 43 | |
|
44 | 44 | |
|
45 | 45 | class FileWrapper(object): |
|
46 | 46 | |
|
47 | 47 | def __init__(self, fd, content_length): |
|
48 | 48 | self.fd = fd |
|
49 | 49 | self.content_length = content_length |
|
50 | 50 | self.remain = content_length |
|
51 | 51 | |
|
52 | 52 | def read(self, size): |
|
53 | 53 | if size <= self.remain: |
|
54 | 54 | try: |
|
55 | 55 | data = self.fd.read(size) |
|
56 | 56 | except socket.error: |
|
57 | 57 | raise IOError(self) |
|
58 | 58 | self.remain -= size |
|
59 | 59 | elif self.remain: |
|
60 | 60 | data = self.fd.read(self.remain) |
|
61 | 61 | self.remain = 0 |
|
62 | 62 | else: |
|
63 | 63 | data = None |
|
64 | 64 | return data |
|
65 | 65 | |
|
66 | 66 | def __repr__(self): |
|
67 | 67 | return '<FileWrapper %s len: %s, read: %s>' % ( |
|
68 | 68 | self.fd, self.content_length, self.content_length - self.remain |
|
69 | 69 | ) |
|
70 | 70 | |
|
71 | 71 | |
|
72 | 72 | class GitRepository(object): |
|
73 | 73 | git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs']) |
|
74 | 74 | commands = ['git-upload-pack', 'git-receive-pack'] |
|
75 | 75 | |
|
76 | 76 | def __init__(self, repo_name, content_path): |
|
77 | 77 | files = set([f.lower() for f in os.listdir(content_path)]) |
|
78 | 78 | if not (self.git_folder_signature.intersection(files) |
|
79 | 79 | == self.git_folder_signature): |
|
80 | 80 | raise OSError('%s missing git signature' % content_path) |
|
81 | 81 | self.content_path = content_path |
|
82 | 82 | self.valid_accepts = ['application/x-%s-result' % |
|
83 | 83 | c for c in self.commands] |
|
84 | 84 | self.repo_name = repo_name |
|
85 | 85 | |
|
86 | 86 | def _get_fixedpath(self, path): |
|
87 | 87 | """ |
|
88 | 88 | Small fix for repo_path |
|
89 | 89 | |
|
90 | 90 | :param path: |
|
91 | 91 | """ |
|
92 | 92 | assert path.startswith('/' + self.repo_name + '/') |
|
93 | 93 | return path[len(self.repo_name) + 2:].strip('/') |
|
94 | 94 | |
|
95 | 95 | def inforefs(self, req, environ): |
|
96 | 96 | """ |
|
97 | 97 | WSGI Response producer for HTTP GET Git Smart |
|
98 | 98 | HTTP /info/refs request. |
|
99 | 99 | """ |
|
100 | 100 | |
|
101 | 101 | git_command = req.GET.get('service') |
|
102 | 102 | if git_command not in self.commands: |
|
103 | 103 | log.debug('command %s not allowed', git_command) |
|
104 | 104 | return exc.HTTPMethodNotAllowed() |
|
105 | 105 | |
|
106 | 106 | # From Documentation/technical/http-protocol.txt shipped with Git: |
|
107 | 107 | # |
|
108 | 108 | # Clients MUST verify the first pkt-line is `# service=$servicename`. |
|
109 | 109 | # Servers MUST set $servicename to be the request parameter value. |
|
110 | 110 | # Servers SHOULD include an LF at the end of this line. |
|
111 | 111 | # Clients MUST ignore an LF at the end of the line. |
|
112 | 112 | # |
|
113 | 113 | # smart_reply = PKT-LINE("# service=$servicename" LF) |
|
114 | 114 | # ref_list |
|
115 | 115 | # "0000" |
|
116 | 116 | server_advert = '# service=%s\n' % git_command |
|
117 | 117 | packet_len = hex(len(server_advert) + 4)[2:].rjust(4, '0').lower() |
|
118 | 118 | _git_path = kallithea.CONFIG.get('git_path', 'git') |
|
119 | 119 | cmd = [_git_path, git_command[4:], |
|
120 | 120 | '--stateless-rpc', '--advertise-refs', self.content_path] |
|
121 | 121 | log.debug('handling cmd %s', cmd) |
|
122 | 122 | try: |
|
123 | 123 | out = subprocessio.SubprocessIOChunker(cmd, |
|
124 | 124 | starting_values=[ascii_bytes(packet_len + server_advert + '0000')] |
|
125 | 125 | ) |
|
126 | 126 | except EnvironmentError as e: |
|
127 | 127 | log.error(traceback.format_exc()) |
|
128 | 128 | raise exc.HTTPExpectationFailed() |
|
129 | 129 | resp = Response() |
|
130 | 130 | resp.content_type = 'application/x-%s-advertisement' % git_command |
|
131 | 131 | resp.charset = None |
|
132 | 132 | resp.app_iter = out |
|
133 | 133 | return resp |
|
134 | 134 | |
|
135 | 135 | def backend(self, req, environ): |
|
136 | 136 | """ |
|
137 | 137 | WSGI Response producer for HTTP POST Git Smart HTTP requests. |
|
138 | 138 | Reads commands and data from HTTP POST's body. |
|
139 | 139 | returns an iterator obj with contents of git command's |
|
140 | 140 | response to stdout |
|
141 | 141 | """ |
|
142 | 142 | _git_path = kallithea.CONFIG.get('git_path', 'git') |
|
143 | 143 | git_command = self._get_fixedpath(req.path_info) |
|
144 | 144 | if git_command not in self.commands: |
|
145 | 145 | log.debug('command %s not allowed', git_command) |
|
146 | 146 | return exc.HTTPMethodNotAllowed() |
|
147 | 147 | |
|
148 | 148 | if 'CONTENT_LENGTH' in environ: |
|
149 | 149 | inputstream = FileWrapper(environ['wsgi.input'], |
|
150 | 150 | req.content_length) |
|
151 | 151 | else: |
|
152 | 152 | inputstream = environ['wsgi.input'] |
|
153 | 153 | |
|
154 | 154 | gitenv = dict(os.environ) |
|
155 | 155 | # forget all configs |
|
156 | 156 | gitenv['GIT_CONFIG_NOGLOBAL'] = '1' |
|
157 | 157 | cmd = [_git_path, git_command[4:], '--stateless-rpc', self.content_path] |
|
158 | 158 | log.debug('handling cmd %s', cmd) |
|
159 | 159 | try: |
|
160 | 160 | out = subprocessio.SubprocessIOChunker( |
|
161 | 161 | cmd, |
|
162 | 162 | inputstream=inputstream, |
|
163 | 163 | env=gitenv, |
|
164 | 164 | cwd=self.content_path, |
|
165 | 165 | ) |
|
166 | 166 | except EnvironmentError as e: |
|
167 | 167 | log.error(traceback.format_exc()) |
|
168 | 168 | raise exc.HTTPExpectationFailed() |
|
169 | 169 | |
|
170 | 170 | if git_command in ['git-receive-pack']: |
|
171 | 171 | # updating refs manually after each push. |
|
172 | 172 | # Needed for pre-1.7.0.4 git clients using regular HTTP mode. |
|
173 | 173 | repo = get_repo(self.content_path) |
|
174 | 174 | if repo: |
|
175 | 175 | update_server_info(repo._repo) |
|
176 | 176 | |
|
177 | 177 | resp = Response() |
|
178 | 178 | resp.content_type = 'application/x-%s-result' % git_command |
|
179 | 179 | resp.charset = None |
|
180 | 180 | resp.app_iter = out |
|
181 | 181 | return resp |
|
182 | 182 | |
|
183 | 183 | def __call__(self, environ, start_response): |
|
184 | 184 | req = Request(environ) |
|
185 | 185 | _path = self._get_fixedpath(req.path_info) |
|
186 | 186 | if _path.startswith('info/refs'): |
|
187 | 187 | app = self.inforefs |
|
188 | 188 | elif req.accept.acceptable_offers(self.valid_accepts): |
|
189 | 189 | app = self.backend |
|
190 | 190 | try: |
|
191 | 191 | resp = app(req, environ) |
|
192 | 192 | except exc.HTTPException as e: |
|
193 | 193 | resp = e |
|
194 | 194 | log.error(traceback.format_exc()) |
|
195 | 195 | except Exception as e: |
|
196 | 196 | log.error(traceback.format_exc()) |
|
197 | 197 | resp = exc.HTTPInternalServerError() |
|
198 | 198 | return resp(environ, start_response) |
|
199 | 199 | |
|
200 | 200 | |
|
201 | 201 | class GitDirectory(object): |
|
202 | 202 | |
|
203 | 203 | def __init__(self, repo_root, repo_name): |
|
204 | 204 | repo_location = os.path.join(repo_root, repo_name) |
|
205 | 205 | if not os.path.isdir(repo_location): |
|
206 | 206 | raise OSError(repo_location) |
|
207 | 207 | |
|
208 | 208 | self.content_path = repo_location |
|
209 | 209 | self.repo_name = repo_name |
|
210 | 210 | self.repo_location = repo_location |
|
211 | 211 | |
|
212 | 212 | def __call__(self, environ, start_response): |
|
213 | 213 | content_path = self.content_path |
|
214 | 214 | try: |
|
215 | 215 | app = GitRepository(self.repo_name, content_path) |
|
216 | 216 | except (AssertionError, OSError): |
|
217 | 217 | content_path = os.path.join(content_path, '.git') |
|
218 | 218 | if os.path.isdir(content_path): |
|
219 | 219 | app = GitRepository(self.repo_name, content_path) |
|
220 | 220 | else: |
|
221 | 221 | return exc.HTTPNotFound()(environ, start_response) |
|
222 | 222 | return app(environ, start_response) |
|
223 | 223 | |
|
224 | 224 | |
|
225 | 225 | def make_wsgi_app(repo_name, repo_root): |
|
226 | 226 | app = GitDirectory(repo_root, repo_name) |
|
227 | 227 | return GunzipFilter(LimitedInputFilter(app)) |
@@ -1,93 +1,93 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | # This program is free software: you can redistribute it and/or modify |
|
3 | 3 | # it under the terms of the GNU General Public License as published by |
|
4 | 4 | # the Free Software Foundation, either version 3 of the License, or |
|
5 | 5 | # (at your option) any later version. |
|
6 | 6 | # |
|
7 | 7 | # This program is distributed in the hope that it will be useful, |
|
8 | 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
9 | 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
10 | 10 | # GNU General Public License for more details. |
|
11 | 11 | # |
|
12 | 12 | # You should have received a copy of the GNU General Public License |
|
13 | 13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
14 | 14 | """ |
|
15 |
kallithea. |
|
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
|
15 | kallithea.config.middleware.simplegit | |
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
|
17 | 17 | |
|
18 | 18 | SimpleGit middleware for handling Git protocol requests (push/clone etc.) |
|
19 | 19 | It's implemented with basic auth function |
|
20 | 20 | |
|
21 | 21 | This file was forked by the Kallithea project in July 2014. |
|
22 | 22 | Original author and date, and relevant copyright and licensing information is below: |
|
23 | 23 | :created_on: Apr 28, 2010 |
|
24 | 24 | :author: marcink |
|
25 | 25 | :copyright: (c) 2013 RhodeCode GmbH, and others. |
|
26 | 26 | :license: GPLv3, see LICENSE.md for more details. |
|
27 | 27 | |
|
28 | 28 | """ |
|
29 | 29 | |
|
30 | 30 | |
|
31 | 31 | import logging |
|
32 | 32 | import re |
|
33 | 33 | |
|
34 | 34 | from kallithea.config.middleware.pygrack import make_wsgi_app |
|
35 | 35 | from kallithea.lib import hooks |
|
36 | 36 | from kallithea.lib.base import BaseVCSController, get_path_info |
|
37 | 37 | |
|
38 | 38 | |
|
39 | 39 | log = logging.getLogger(__name__) |
|
40 | 40 | |
|
41 | 41 | |
|
42 | 42 | GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)$') |
|
43 | 43 | |
|
44 | 44 | |
|
45 | 45 | cmd_mapping = { |
|
46 | 46 | 'git-receive-pack': 'push', |
|
47 | 47 | 'git-upload-pack': 'pull', |
|
48 | 48 | } |
|
49 | 49 | |
|
50 | 50 | |
|
51 | 51 | class SimpleGit(BaseVCSController): |
|
52 | 52 | |
|
53 | 53 | scm_alias = 'git' |
|
54 | 54 | |
|
55 | 55 | @classmethod |
|
56 | 56 | def parse_request(cls, environ): |
|
57 | 57 | path_info = get_path_info(environ) |
|
58 | 58 | m = GIT_PROTO_PAT.match(path_info) |
|
59 | 59 | if m is None: |
|
60 | 60 | return None |
|
61 | 61 | |
|
62 | 62 | class parsed_request(object): |
|
63 | 63 | # See https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols#_the_smart_protocol |
|
64 | 64 | repo_name = m.group(1).rstrip('/') |
|
65 | 65 | cmd = m.group(2) |
|
66 | 66 | |
|
67 | 67 | query_string = environ['QUERY_STRING'] |
|
68 | 68 | if cmd == 'info/refs' and query_string.startswith('service='): |
|
69 | 69 | service = query_string.split('=', 1)[1] |
|
70 | 70 | action = cmd_mapping.get(service) |
|
71 | 71 | else: |
|
72 | 72 | service = None |
|
73 | 73 | action = cmd_mapping.get(cmd) |
|
74 | 74 | |
|
75 | 75 | return parsed_request |
|
76 | 76 | |
|
77 | 77 | def _make_app(self, parsed_request): |
|
78 | 78 | """ |
|
79 | 79 | Return a pygrack wsgi application. |
|
80 | 80 | """ |
|
81 | 81 | pygrack_app = make_wsgi_app(parsed_request.repo_name, self.basepath) |
|
82 | 82 | |
|
83 | 83 | def wrapper_app(environ, start_response): |
|
84 | 84 | if (parsed_request.cmd == 'info/refs' and |
|
85 | 85 | parsed_request.service == 'git-upload-pack' |
|
86 | 86 | ): |
|
87 | 87 | # Run hooks like Mercurial outgoing.kallithea_pull_action does |
|
88 | 88 | hooks.log_pull_action() |
|
89 | 89 | # Note: push hooks are handled by post-receive hook |
|
90 | 90 | |
|
91 | 91 | return pygrack_app(environ, start_response) |
|
92 | 92 | |
|
93 | 93 | return wrapper_app |
@@ -1,149 +1,149 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | # This program is free software: you can redistribute it and/or modify |
|
3 | 3 | # it under the terms of the GNU General Public License as published by |
|
4 | 4 | # the Free Software Foundation, either version 3 of the License, or |
|
5 | 5 | # (at your option) any later version. |
|
6 | 6 | # |
|
7 | 7 | # This program is distributed in the hope that it will be useful, |
|
8 | 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
9 | 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
10 | 10 | # GNU General Public License for more details. |
|
11 | 11 | # |
|
12 | 12 | # You should have received a copy of the GNU General Public License |
|
13 | 13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
14 | 14 | """ |
|
15 |
kallithea. |
|
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
|
15 | kallithea.config.middleware.simplehg | |
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
|
17 | 17 | |
|
18 | 18 | SimpleHg middleware for handling Mercurial protocol requests (push/clone etc.). |
|
19 | 19 | It's implemented with basic auth function |
|
20 | 20 | |
|
21 | 21 | This file was forked by the Kallithea project in July 2014. |
|
22 | 22 | Original author and date, and relevant copyright and licensing information is below: |
|
23 | 23 | :created_on: Apr 28, 2010 |
|
24 | 24 | :author: marcink |
|
25 | 25 | :copyright: (c) 2013 RhodeCode GmbH, and others. |
|
26 | 26 | :license: GPLv3, see LICENSE.md for more details. |
|
27 | 27 | |
|
28 | 28 | """ |
|
29 | 29 | |
|
30 | 30 | |
|
31 | 31 | import logging |
|
32 | 32 | import os |
|
33 | 33 | import urllib.parse |
|
34 | 34 | |
|
35 | 35 | import mercurial.hgweb |
|
36 | 36 | |
|
37 | 37 | from kallithea.lib.base import BaseVCSController, get_path_info |
|
38 | 38 | from kallithea.lib.utils import make_ui |
|
39 | 39 | from kallithea.lib.utils2 import safe_bytes |
|
40 | 40 | |
|
41 | 41 | |
|
42 | 42 | log = logging.getLogger(__name__) |
|
43 | 43 | |
|
44 | 44 | |
|
45 | 45 | def get_header_hgarg(environ): |
|
46 | 46 | """Decode the special Mercurial encoding of big requests over multiple headers. |
|
47 | 47 | >>> get_header_hgarg({}) |
|
48 | 48 | '' |
|
49 | 49 | >>> get_header_hgarg({'HTTP_X_HGARG_0': ' ', 'HTTP_X_HGARG_1': 'a','HTTP_X_HGARG_2': '','HTTP_X_HGARG_3': 'b+c %20'}) |
|
50 | 50 | 'ab+c %20' |
|
51 | 51 | """ |
|
52 | 52 | chunks = [] |
|
53 | 53 | i = 1 |
|
54 | 54 | while True: |
|
55 | 55 | v = environ.get('HTTP_X_HGARG_%d' % i) |
|
56 | 56 | if v is None: |
|
57 | 57 | break |
|
58 | 58 | chunks.append(v) |
|
59 | 59 | i += 1 |
|
60 | 60 | return ''.join(chunks) |
|
61 | 61 | |
|
62 | 62 | |
|
63 | 63 | cmd_mapping = { |
|
64 | 64 | # 'batch' is not in this list - it is handled explicitly |
|
65 | 65 | 'between': 'pull', |
|
66 | 66 | 'branches': 'pull', |
|
67 | 67 | 'branchmap': 'pull', |
|
68 | 68 | 'capabilities': 'pull', |
|
69 | 69 | 'changegroup': 'pull', |
|
70 | 70 | 'changegroupsubset': 'pull', |
|
71 | 71 | 'changesetdata': 'pull', |
|
72 | 72 | 'clonebundles': 'pull', |
|
73 | 73 | 'debugwireargs': 'pull', |
|
74 | 74 | 'filedata': 'pull', |
|
75 | 75 | 'getbundle': 'pull', |
|
76 | 76 | 'getlfile': 'pull', |
|
77 | 77 | 'heads': 'pull', |
|
78 | 78 | 'hello': 'pull', |
|
79 | 79 | 'known': 'pull', |
|
80 | 80 | 'lheads': 'pull', |
|
81 | 81 | 'listkeys': 'pull', |
|
82 | 82 | 'lookup': 'pull', |
|
83 | 83 | 'manifestdata': 'pull', |
|
84 | 84 | 'narrow_widen': 'pull', |
|
85 | 85 | 'protocaps': 'pull', |
|
86 | 86 | 'statlfile': 'pull', |
|
87 | 87 | 'stream_out': 'pull', |
|
88 | 88 | 'pushkey': 'push', |
|
89 | 89 | 'putlfile': 'push', |
|
90 | 90 | 'unbundle': 'push', |
|
91 | 91 | } |
|
92 | 92 | |
|
93 | 93 | |
|
94 | 94 | class SimpleHg(BaseVCSController): |
|
95 | 95 | |
|
96 | 96 | scm_alias = 'hg' |
|
97 | 97 | |
|
98 | 98 | @classmethod |
|
99 | 99 | def parse_request(cls, environ): |
|
100 | 100 | http_accept = environ.get('HTTP_ACCEPT', '') |
|
101 | 101 | if not http_accept.startswith('application/mercurial'): |
|
102 | 102 | return None |
|
103 | 103 | path_info = get_path_info(environ) |
|
104 | 104 | if not path_info.startswith('/'): # it must! |
|
105 | 105 | return None |
|
106 | 106 | |
|
107 | 107 | class parsed_request(object): |
|
108 | 108 | repo_name = path_info[1:].rstrip('/') |
|
109 | 109 | |
|
110 | 110 | query_string = environ['QUERY_STRING'] |
|
111 | 111 | |
|
112 | 112 | action = None |
|
113 | 113 | for qry in query_string.split('&'): |
|
114 | 114 | parts = qry.split('=', 1) |
|
115 | 115 | if len(parts) == 2 and parts[0] == 'cmd': |
|
116 | 116 | cmd = parts[1] |
|
117 | 117 | if cmd == 'batch': |
|
118 | 118 | hgarg = get_header_hgarg(environ) |
|
119 | 119 | if not hgarg.startswith('cmds='): |
|
120 | 120 | action = 'push' # paranoid and safe |
|
121 | 121 | break |
|
122 | 122 | action = 'pull' |
|
123 | 123 | for cmd_arg in hgarg[5:].split(';'): |
|
124 | 124 | cmd, _args = urllib.parse.unquote_plus(cmd_arg).split(' ', 1) |
|
125 | 125 | op = cmd_mapping.get(cmd, 'push') |
|
126 | 126 | if op != 'pull': |
|
127 | 127 | assert op == 'push' |
|
128 | 128 | action = 'push' |
|
129 | 129 | break |
|
130 | 130 | else: |
|
131 | 131 | action = cmd_mapping.get(cmd, 'push') |
|
132 | 132 | break # only process one cmd |
|
133 | 133 | |
|
134 | 134 | return parsed_request |
|
135 | 135 | |
|
136 | 136 | def _make_app(self, parsed_request): |
|
137 | 137 | """ |
|
138 | 138 | Make an hgweb wsgi application. |
|
139 | 139 | """ |
|
140 | 140 | repo_name = parsed_request.repo_name |
|
141 | 141 | repo_path = os.path.join(self.basepath, repo_name) |
|
142 | 142 | baseui = make_ui(repo_path=repo_path) |
|
143 | 143 | hgweb_app = mercurial.hgweb.hgweb(safe_bytes(repo_path), name=safe_bytes(repo_name), baseui=baseui) |
|
144 | 144 | |
|
145 | 145 | def wrapper_app(environ, start_response): |
|
146 | 146 | environ['REPO_NAME'] = repo_name # used by mercurial.hgweb.hgweb |
|
147 | 147 | return hgweb_app(environ, start_response) |
|
148 | 148 | |
|
149 | 149 | return wrapper_app |
@@ -1,102 +1,102 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | # This program is free software: you can redistribute it and/or modify |
|
3 | 3 | # it under the terms of the GNU General Public License as published by |
|
4 | 4 | # the Free Software Foundation, either version 3 of the License, or |
|
5 | 5 | # (at your option) any later version. |
|
6 | 6 | # |
|
7 | 7 | # This program is distributed in the hope that it will be useful, |
|
8 | 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
9 | 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
10 | 10 | # GNU General Public License for more details. |
|
11 | 11 | # |
|
12 | 12 | # You should have received a copy of the GNU General Public License |
|
13 | 13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
14 | 14 | """ |
|
15 |
kallithea. |
|
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
|
15 | kallithea.config.middleware.wrapper | |
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
|
17 | 17 | |
|
18 | 18 | Wrap app to measure request and response time ... all the way to the response |
|
19 | 19 | WSGI iterator has been closed. |
|
20 | 20 | |
|
21 | 21 | This file was forked by the Kallithea project in July 2014. |
|
22 | 22 | Original author and date, and relevant copyright and licensing information is below: |
|
23 | 23 | :created_on: May 23, 2013 |
|
24 | 24 | :author: marcink |
|
25 | 25 | :copyright: (c) 2013 RhodeCode GmbH, and others. |
|
26 | 26 | :license: GPLv3, see LICENSE.md for more details. |
|
27 | 27 | """ |
|
28 | 28 | |
|
29 | 29 | import logging |
|
30 | 30 | import time |
|
31 | 31 | |
|
32 | 32 | from kallithea.lib.base import _get_ip_addr, get_path_info |
|
33 | 33 | |
|
34 | 34 | |
|
35 | 35 | log = logging.getLogger(__name__) |
|
36 | 36 | |
|
37 | 37 | |
|
38 | 38 | class Meter: |
|
39 | 39 | |
|
40 | 40 | def __init__(self, start_response): |
|
41 | 41 | self._start_response = start_response |
|
42 | 42 | self._start = time.time() |
|
43 | 43 | self.status = None |
|
44 | 44 | self._size = 0 |
|
45 | 45 | |
|
46 | 46 | def duration(self): |
|
47 | 47 | return time.time() - self._start |
|
48 | 48 | |
|
49 | 49 | def start_response(self, status, response_headers, exc_info=None): |
|
50 | 50 | self.status = status |
|
51 | 51 | write = self._start_response(status, response_headers, exc_info) |
|
52 | 52 | def metered_write(s): |
|
53 | 53 | self.measure(s) |
|
54 | 54 | write(s) |
|
55 | 55 | return metered_write |
|
56 | 56 | |
|
57 | 57 | def measure(self, chunk): |
|
58 | 58 | self._size += len(chunk) |
|
59 | 59 | |
|
60 | 60 | def size(self): |
|
61 | 61 | return self._size |
|
62 | 62 | |
|
63 | 63 | |
|
64 | 64 | class ResultIter: |
|
65 | 65 | |
|
66 | 66 | def __init__(self, result, meter, description): |
|
67 | 67 | self._result_close = getattr(result, 'close', None) or (lambda: None) |
|
68 | 68 | self._next = iter(result).__next__ |
|
69 | 69 | self._meter = meter |
|
70 | 70 | self._description = description |
|
71 | 71 | |
|
72 | 72 | def __iter__(self): |
|
73 | 73 | return self |
|
74 | 74 | |
|
75 | 75 | def __next__(self): |
|
76 | 76 | chunk = self._next() |
|
77 | 77 | self._meter.measure(chunk) |
|
78 | 78 | return chunk |
|
79 | 79 | |
|
80 | 80 | def close(self): |
|
81 | 81 | self._result_close() |
|
82 | 82 | log.info("%s responded %r after %.3fs with %s bytes", self._description, self._meter.status, self._meter.duration(), self._meter.size()) |
|
83 | 83 | |
|
84 | 84 | |
|
85 | 85 | class RequestWrapper(object): |
|
86 | 86 | |
|
87 | 87 | def __init__(self, app, config): |
|
88 | 88 | self.application = app |
|
89 | 89 | self.config = config |
|
90 | 90 | |
|
91 | 91 | def __call__(self, environ, start_response): |
|
92 | 92 | meter = Meter(start_response) |
|
93 | 93 | description = "Request from %s for %s" % ( |
|
94 | 94 | _get_ip_addr(environ), |
|
95 | 95 | get_path_info(environ), |
|
96 | 96 | ) |
|
97 | 97 | log.info("%s received", description) |
|
98 | 98 | try: |
|
99 | 99 | result = self.application(environ, meter.start_response) |
|
100 | 100 | finally: |
|
101 | 101 | log.info("%s responding %r after %.3fs", description, meter.status, meter.duration()) |
|
102 | 102 | return ResultIter(result, meter, description) |
@@ -1,69 +1,69 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | # This program is free software: you can redistribute it and/or modify |
|
3 | 3 | # it under the terms of the GNU General Public License as published by |
|
4 | 4 | # the Free Software Foundation, either version 3 of the License, or |
|
5 | 5 | # (at your option) any later version. |
|
6 | 6 | # |
|
7 | 7 | # This program is distributed in the hope that it will be useful, |
|
8 | 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
9 | 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
10 | 10 | # GNU General Public License for more details. |
|
11 | 11 | # |
|
12 | 12 | # You should have received a copy of the GNU General Public License |
|
13 | 13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
14 | 14 | """ |
|
15 | 15 | kallithea.lib.conf |
|
16 |
~~~~~~~~~~~~~~~~~~ |
|
|
16 | ~~~~~~~~~~~~~~~~~~ | |
|
17 | 17 | |
|
18 | 18 | Various config settings for Kallithea |
|
19 | 19 | |
|
20 | 20 | This file was forked by the Kallithea project in July 2014. |
|
21 | 21 | Original author and date, and relevant copyright and licensing information is below: |
|
22 | 22 | :created_on: Mar 7, 2012 |
|
23 | 23 | :author: marcink |
|
24 | 24 | :copyright: (c) 2013 RhodeCode GmbH, and others. |
|
25 | 25 | :license: GPLv3, see LICENSE.md for more details. |
|
26 | 26 | """ |
|
27 | 27 | |
|
28 | 28 | from kallithea.lib import pygmentsutils |
|
29 | 29 | |
|
30 | 30 | |
|
31 | 31 | # language map is also used by whoosh indexer, which for those specified |
|
32 | 32 | # extensions will index it's content |
|
33 | 33 | LANGUAGES_EXTENSIONS_MAP = pygmentsutils.get_extension_descriptions() |
|
34 | 34 | |
|
35 | 35 | # Whoosh index targets |
|
36 | 36 | |
|
37 | 37 | # Extensions we want to index content of using whoosh |
|
38 | 38 | INDEX_EXTENSIONS = list(LANGUAGES_EXTENSIONS_MAP) |
|
39 | 39 | |
|
40 | 40 | # Filenames we want to index content of using whoosh |
|
41 | 41 | INDEX_FILENAMES = pygmentsutils.get_index_filenames() |
|
42 | 42 | |
|
43 | 43 | # list of readme files to search in file tree and display in summary |
|
44 | 44 | # attached weights defines the search order lower is first |
|
45 | 45 | ALL_READMES = [ |
|
46 | 46 | ('readme', 0), ('README', 0), ('Readme', 0), |
|
47 | 47 | ('doc/readme', 1), ('doc/README', 1), ('doc/Readme', 1), |
|
48 | 48 | ('Docs/readme', 2), ('Docs/README', 2), ('Docs/Readme', 2), |
|
49 | 49 | ('DOCS/readme', 2), ('DOCS/README', 2), ('DOCS/Readme', 2), |
|
50 | 50 | ('docs/readme', 2), ('docs/README', 2), ('docs/Readme', 2), |
|
51 | 51 | ] |
|
52 | 52 | |
|
53 | 53 | # extension together with weights to search lower is first |
|
54 | 54 | RST_EXTS = [ |
|
55 | 55 | ('', 0), ('.rst', 1), ('.rest', 1), |
|
56 | 56 | ('.RST', 2), ('.REST', 2), |
|
57 | 57 | ('.txt', 3), ('.TXT', 3) |
|
58 | 58 | ] |
|
59 | 59 | |
|
60 | 60 | MARKDOWN_EXTS = [ |
|
61 | 61 | ('.md', 1), ('.MD', 1), |
|
62 | 62 | ('.mkdn', 2), ('.MKDN', 2), |
|
63 | 63 | ('.mdown', 3), ('.MDOWN', 3), |
|
64 | 64 | ('.markdown', 4), ('.MARKDOWN', 4) |
|
65 | 65 | ] |
|
66 | 66 | |
|
67 | 67 | PLAIN_EXTS = [('.text', 2), ('.TEXT', 2)] |
|
68 | 68 | |
|
69 | 69 | ALL_EXTS = MARKDOWN_EXTS + RST_EXTS + PLAIN_EXTS |
@@ -1,258 +1,258 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | # This program is free software: you can redistribute it and/or modify |
|
3 | 3 | # it under the terms of the GNU General Public License as published by |
|
4 | 4 | # the Free Software Foundation, either version 3 of the License, or |
|
5 | 5 | # (at your option) any later version. |
|
6 | 6 | # |
|
7 | 7 | # This program is distributed in the hope that it will be useful, |
|
8 | 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
9 | 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
10 | 10 | # GNU General Public License for more details. |
|
11 | 11 | # |
|
12 | 12 | # You should have received a copy of the GNU General Public License |
|
13 | 13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
14 | 14 | """ |
|
15 | 15 | kallithea.lib.webutils |
|
16 | ~~~~~~~~~~~~~~~~~~~~ | |
|
16 | ~~~~~~~~~~~~~~~~~~~~~~ | |
|
17 | 17 | |
|
18 | 18 | Helper functions that may rely on the current WSGI request, exposed in the TG2 |
|
19 | 19 | thread-local "global" variables. It should have few dependencies so it can be |
|
20 | 20 | imported anywhere - just like the global variables can be used everywhere. |
|
21 | 21 | """ |
|
22 | 22 | |
|
23 | 23 | import logging |
|
24 | 24 | import random |
|
25 | 25 | |
|
26 | 26 | from tg import request, session |
|
27 | 27 | from webhelpers2.html import HTML, escape, literal |
|
28 | 28 | from webhelpers2.html.tags import NotGiven, Option, Options, _input |
|
29 | 29 | from webhelpers2.html.tags import _make_safe_id_component as safeid |
|
30 | 30 | from webhelpers2.html.tags import checkbox, end_form |
|
31 | 31 | from webhelpers2.html.tags import form as insecure_form |
|
32 | 32 | from webhelpers2.html.tags import hidden, link_to, password, radio |
|
33 | 33 | from webhelpers2.html.tags import select as webhelpers2_select |
|
34 | 34 | from webhelpers2.html.tags import submit, text, textarea |
|
35 | 35 | from webhelpers2.number import format_byte_size |
|
36 | 36 | from webhelpers2.text import chop_at, truncate, wrap_paragraphs |
|
37 | 37 | |
|
38 | 38 | import kallithea |
|
39 | 39 | |
|
40 | 40 | |
|
41 | 41 | log = logging.getLogger(__name__) |
|
42 | 42 | |
|
43 | 43 | |
|
44 | 44 | # mute pyflakes "imported but unused" |
|
45 | 45 | assert Option |
|
46 | 46 | assert checkbox |
|
47 | 47 | assert chop_at |
|
48 | 48 | assert end_form |
|
49 | 49 | assert escape |
|
50 | 50 | assert format_byte_size |
|
51 | 51 | assert link_to |
|
52 | 52 | assert literal |
|
53 | 53 | assert password |
|
54 | 54 | assert radio |
|
55 | 55 | assert safeid |
|
56 | 56 | assert submit |
|
57 | 57 | assert text |
|
58 | 58 | assert textarea |
|
59 | 59 | assert truncate |
|
60 | 60 | assert wrap_paragraphs |
|
61 | 61 | |
|
62 | 62 | |
|
63 | 63 | # |
|
64 | 64 | # General Kallithea URL handling |
|
65 | 65 | # |
|
66 | 66 | |
|
67 | 67 | class UrlGenerator(object): |
|
68 | 68 | """Emulate pylons.url in providing a wrapper around routes.url |
|
69 | 69 | |
|
70 | 70 | This code was added during migration from Pylons to Turbogears2. Pylons |
|
71 | 71 | already provided a wrapper like this, but Turbogears2 does not. |
|
72 | 72 | |
|
73 | 73 | When the routing of Kallithea is changed to use less Routes and more |
|
74 | 74 | Turbogears2-style routing, this class may disappear or change. |
|
75 | 75 | |
|
76 | 76 | url() (the __call__ method) returns the URL based on a route name and |
|
77 | 77 | arguments. |
|
78 | 78 | url.current() returns the URL of the current page with arguments applied. |
|
79 | 79 | |
|
80 | 80 | Refer to documentation of Routes for details: |
|
81 | 81 | https://routes.readthedocs.io/en/latest/generating.html#generation |
|
82 | 82 | """ |
|
83 | 83 | def __call__(self, *args, **kwargs): |
|
84 | 84 | return request.environ['routes.url'](*args, **kwargs) |
|
85 | 85 | |
|
86 | 86 | def current(self, *args, **kwargs): |
|
87 | 87 | return request.environ['routes.url'].current(*args, **kwargs) |
|
88 | 88 | |
|
89 | 89 | |
|
90 | 90 | url = UrlGenerator() |
|
91 | 91 | |
|
92 | 92 | |
|
93 | 93 | def canonical_url(*args, **kargs): |
|
94 | 94 | '''Like url(x, qualified=True), but returns url that not only is qualified |
|
95 | 95 | but also canonical, as configured in canonical_url''' |
|
96 | 96 | try: |
|
97 | 97 | parts = kallithea.CONFIG.get('canonical_url', '').split('://', 1) |
|
98 | 98 | kargs['host'] = parts[1] |
|
99 | 99 | kargs['protocol'] = parts[0] |
|
100 | 100 | except IndexError: |
|
101 | 101 | kargs['qualified'] = True |
|
102 | 102 | return url(*args, **kargs) |
|
103 | 103 | |
|
104 | 104 | |
|
105 | 105 | def canonical_hostname(): |
|
106 | 106 | '''Return canonical hostname of system''' |
|
107 | 107 | try: |
|
108 | 108 | parts = kallithea.CONFIG.get('canonical_url', '').split('://', 1) |
|
109 | 109 | return parts[1].split('/', 1)[0] |
|
110 | 110 | except IndexError: |
|
111 | 111 | parts = url('home', qualified=True).split('://', 1) |
|
112 | 112 | return parts[1].split('/', 1)[0] |
|
113 | 113 | |
|
114 | 114 | |
|
115 | 115 | # |
|
116 | 116 | # Custom Webhelpers2 stuff |
|
117 | 117 | # |
|
118 | 118 | |
|
119 | 119 | def html_escape(s): |
|
120 | 120 | """Return string with all html escaped. |
|
121 | 121 | This is also safe for javascript in html but not necessarily correct. |
|
122 | 122 | """ |
|
123 | 123 | return (s |
|
124 | 124 | .replace('&', '&') |
|
125 | 125 | .replace(">", ">") |
|
126 | 126 | .replace("<", "<") |
|
127 | 127 | .replace('"', """) |
|
128 | 128 | .replace("'", "'") # Note: this is HTML5 not HTML4 and might not work in mails |
|
129 | 129 | ) |
|
130 | 130 | |
|
131 | 131 | |
|
132 | 132 | def reset(name, value, id=NotGiven, **attrs): |
|
133 | 133 | """Create a reset button, similar to webhelpers2.html.tags.submit .""" |
|
134 | 134 | return _input("reset", name, value, id, attrs) |
|
135 | 135 | |
|
136 | 136 | |
|
137 | 137 | def select(name, selected_values, options, id=NotGiven, **attrs): |
|
138 | 138 | """Convenient wrapper of webhelpers2 to let it accept options as a tuple list""" |
|
139 | 139 | if isinstance(options, list): |
|
140 | 140 | option_list = options |
|
141 | 141 | # Handle old value,label lists ... where value also can be value,label lists |
|
142 | 142 | options = Options() |
|
143 | 143 | for x in option_list: |
|
144 | 144 | if isinstance(x, tuple) and len(x) == 2: |
|
145 | 145 | value, label = x |
|
146 | 146 | elif isinstance(x, str): |
|
147 | 147 | value = label = x |
|
148 | 148 | else: |
|
149 | 149 | log.error('invalid select option %r', x) |
|
150 | 150 | raise |
|
151 | 151 | if isinstance(value, list): |
|
152 | 152 | og = options.add_optgroup(label) |
|
153 | 153 | for x in value: |
|
154 | 154 | if isinstance(x, tuple) and len(x) == 2: |
|
155 | 155 | group_value, group_label = x |
|
156 | 156 | elif isinstance(x, str): |
|
157 | 157 | group_value = group_label = x |
|
158 | 158 | else: |
|
159 | 159 | log.error('invalid select option %r', x) |
|
160 | 160 | raise |
|
161 | 161 | og.add_option(group_label, group_value) |
|
162 | 162 | else: |
|
163 | 163 | options.add_option(label, value) |
|
164 | 164 | return webhelpers2_select(name, selected_values, options, id=id, **attrs) |
|
165 | 165 | |
|
166 | 166 | |
|
167 | 167 | session_csrf_secret_name = "_session_csrf_secret_token" |
|
168 | 168 | |
|
169 | 169 | def session_csrf_secret_token(): |
|
170 | 170 | """Return (and create) the current session's CSRF protection token.""" |
|
171 | 171 | if not session_csrf_secret_name in session: |
|
172 | 172 | session[session_csrf_secret_name] = str(random.getrandbits(128)) |
|
173 | 173 | session.save() |
|
174 | 174 | return session[session_csrf_secret_name] |
|
175 | 175 | |
|
176 | 176 | def form(url, method="post", **attrs): |
|
177 | 177 | """Like webhelpers.html.tags.form , but automatically adding |
|
178 | 178 | session_csrf_secret_token for POST. The secret is thus never leaked in GET |
|
179 | 179 | URLs. |
|
180 | 180 | """ |
|
181 | 181 | form = insecure_form(url, method, **attrs) |
|
182 | 182 | if method.lower() == 'get': |
|
183 | 183 | return form |
|
184 | 184 | return form + HTML.div(hidden(session_csrf_secret_name, session_csrf_secret_token()), style="display: none;") |
|
185 | 185 | |
|
186 | 186 | |
|
187 | 187 | # |
|
188 | 188 | # Flash messages, stored in cookie |
|
189 | 189 | # |
|
190 | 190 | |
|
191 | 191 | class _Message(object): |
|
192 | 192 | """A message returned by ``pop_flash_messages()``. |
|
193 | 193 | |
|
194 | 194 | Converting the message to a string returns the message text. Instances |
|
195 | 195 | also have the following attributes: |
|
196 | 196 | |
|
197 | 197 | * ``category``: the category specified when the message was created. |
|
198 | 198 | * ``message``: the html-safe message text. |
|
199 | 199 | """ |
|
200 | 200 | |
|
201 | 201 | def __init__(self, category, message): |
|
202 | 202 | self.category = category |
|
203 | 203 | self.message = message |
|
204 | 204 | |
|
205 | 205 | |
|
206 | 206 | def _session_flash_messages(append=None, clear=False): |
|
207 | 207 | """Manage a message queue in tg.session: return the current message queue |
|
208 | 208 | after appending the given message, and possibly clearing the queue.""" |
|
209 | 209 | key = 'flash' |
|
210 | 210 | if key in session: |
|
211 | 211 | flash_messages = session[key] |
|
212 | 212 | else: |
|
213 | 213 | if append is None: # common fast path - also used for clearing empty queue |
|
214 | 214 | return [] # don't bother saving |
|
215 | 215 | flash_messages = [] |
|
216 | 216 | session[key] = flash_messages |
|
217 | 217 | if append is not None and append not in flash_messages: |
|
218 | 218 | flash_messages.append(append) |
|
219 | 219 | if clear: |
|
220 | 220 | session.pop(key, None) |
|
221 | 221 | session.save() |
|
222 | 222 | return flash_messages |
|
223 | 223 | |
|
224 | 224 | |
|
225 | 225 | def flash(message, category, logf=None): |
|
226 | 226 | """ |
|
227 | 227 | Show a message to the user _and_ log it through the specified function |
|
228 | 228 | |
|
229 | 229 | category: notice (default), warning, error, success |
|
230 | 230 | logf: a custom log function - such as log.debug |
|
231 | 231 | |
|
232 | 232 | logf defaults to log.info, unless category equals 'success', in which |
|
233 | 233 | case logf defaults to log.debug. |
|
234 | 234 | """ |
|
235 | 235 | assert category in ('error', 'success', 'warning'), category |
|
236 | 236 | if hasattr(message, '__html__'): |
|
237 | 237 | # render to HTML for storing in cookie |
|
238 | 238 | safe_message = str(message) |
|
239 | 239 | else: |
|
240 | 240 | # Apply str - the message might be an exception with __str__ |
|
241 | 241 | # Escape, so we can trust the result without further escaping, without any risk of injection |
|
242 | 242 | safe_message = html_escape(str(message)) |
|
243 | 243 | if logf is None: |
|
244 | 244 | logf = log.info |
|
245 | 245 | if category == 'success': |
|
246 | 246 | logf = log.debug |
|
247 | 247 | |
|
248 | 248 | logf('Flash %s: %s', category, safe_message) |
|
249 | 249 | |
|
250 | 250 | _session_flash_messages(append=(category, safe_message)) |
|
251 | 251 | |
|
252 | 252 | |
|
253 | 253 | def pop_flash_messages(): |
|
254 | 254 | """Return all accumulated messages and delete them from the session. |
|
255 | 255 | |
|
256 | 256 | The return value is a list of ``Message`` objects. |
|
257 | 257 | """ |
|
258 | 258 | return [_Message(category, message) for category, message in _session_flash_messages(clear=True)] |
@@ -1,90 +1,90 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | # This program is free software: you can redistribute it and/or modify |
|
3 | 3 | # it under the terms of the GNU General Public License as published by |
|
4 | 4 | # the Free Software Foundation, either version 3 of the License, or |
|
5 | 5 | # (at your option) any later version. |
|
6 | 6 | # |
|
7 | 7 | # This program is distributed in the hope that it will be useful, |
|
8 | 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
9 | 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
10 | 10 | # GNU General Public License for more details. |
|
11 | 11 | # |
|
12 | 12 | # You should have received a copy of the GNU General Public License |
|
13 | 13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
14 | 14 | """ |
|
15 |
kallithea.model. |
|
|
16 | ~~~~~~~~~~~~~~~~~~ | |
|
15 | kallithea.model.userlog | |
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~ | |
|
17 | 17 | |
|
18 | 18 | Database Models for Kallithea |
|
19 | 19 | |
|
20 | 20 | This file was forked by the Kallithea project in July 2014. |
|
21 | 21 | Original author and date, and relevant copyright and licensing information is below: |
|
22 | 22 | :created_on: Apr 08, 2010 |
|
23 | 23 | :author: marcink |
|
24 | 24 | :copyright: (c) 2013 RhodeCode GmbH, and others. |
|
25 | 25 | :license: GPLv3, see LICENSE.md for more details. |
|
26 | 26 | """ |
|
27 | 27 | |
|
28 | 28 | |
|
29 | 29 | import datetime |
|
30 | 30 | import logging |
|
31 | 31 | |
|
32 | 32 | from kallithea.lib.utils2 import get_current_authuser |
|
33 | 33 | from kallithea.model import db, meta |
|
34 | 34 | |
|
35 | 35 | |
|
36 | 36 | log = logging.getLogger(__name__) |
|
37 | 37 | |
|
38 | 38 | |
|
39 | 39 | def action_logger(user, action, repo, ipaddr='', commit=False): |
|
40 | 40 | """ |
|
41 | 41 | Action logger for various actions made by users |
|
42 | 42 | |
|
43 | 43 | :param user: user that made this action, can be a unique username string or |
|
44 | 44 | object containing user_id attribute |
|
45 | 45 | :param action: action to log, should be on of predefined unique actions for |
|
46 | 46 | easy translations |
|
47 | 47 | :param repo: string name of repository or object containing repo_id, |
|
48 | 48 | that action was made on |
|
49 | 49 | :param ipaddr: optional IP address from what the action was made |
|
50 | 50 | |
|
51 | 51 | """ |
|
52 | 52 | |
|
53 | 53 | # if we don't get explicit IP address try to get one from registered user |
|
54 | 54 | # in tmpl context var |
|
55 | 55 | if not ipaddr: |
|
56 | 56 | ipaddr = getattr(get_current_authuser(), 'ip_addr', '') |
|
57 | 57 | |
|
58 | 58 | if getattr(user, 'user_id', None): |
|
59 | 59 | user_obj = db.User.get(user.user_id) |
|
60 | 60 | elif isinstance(user, str): |
|
61 | 61 | user_obj = db.User.get_by_username(user) |
|
62 | 62 | else: |
|
63 | 63 | raise Exception('You have to provide a user object or a username') |
|
64 | 64 | |
|
65 | 65 | if getattr(repo, 'repo_id', None): |
|
66 | 66 | repo_obj = db.Repository.get(repo.repo_id) |
|
67 | 67 | repo_name = repo_obj.repo_name |
|
68 | 68 | elif isinstance(repo, str): |
|
69 | 69 | repo_name = repo.lstrip('/') |
|
70 | 70 | repo_obj = db.Repository.get_by_repo_name(repo_name) |
|
71 | 71 | else: |
|
72 | 72 | repo_obj = None |
|
73 | 73 | repo_name = '' |
|
74 | 74 | |
|
75 | 75 | user_log = db.UserLog() |
|
76 | 76 | user_log.user_id = user_obj.user_id |
|
77 | 77 | user_log.username = user_obj.username |
|
78 | 78 | user_log.action = action |
|
79 | 79 | |
|
80 | 80 | user_log.repository = repo_obj |
|
81 | 81 | user_log.repository_name = repo_name |
|
82 | 82 | |
|
83 | 83 | user_log.action_date = datetime.datetime.now() |
|
84 | 84 | user_log.user_ip = ipaddr |
|
85 | 85 | meta.Session().add(user_log) |
|
86 | 86 | |
|
87 | 87 | log.info('Logging action:%s on %s by user:%s ip:%s', |
|
88 | 88 | action, repo, user_obj, ipaddr) |
|
89 | 89 | if commit: |
|
90 | 90 | meta.Session().commit() |
@@ -1,13 +1,14 b'' | |||
|
1 | 1 | #!/bin/sh |
|
2 | 2 | |
|
3 | 3 | # Convenience script for running various idempotent source code cleanup scripts |
|
4 | 4 | |
|
5 | 5 | set -e |
|
6 | 6 | set -x |
|
7 | 7 | |
|
8 | 8 | scripts/docs-headings.py |
|
9 | 9 | scripts/generate-ini.py |
|
10 | 10 | scripts/whitespacecleanup.sh |
|
11 | hg files 'set:!binary()&grep("^#!.*python")' 'set:**.py' | xargs scripts/source_format.py | |
|
11 | 12 | |
|
12 | 13 | hg files 'set:!binary()&grep("^#!.*python")' 'set:**.py' | xargs scripts/pyflakes |
|
13 | 14 | echo "no blocking problems found by $0" |
General Comments 0
You need to be logged in to leave comments.
Login now