Files @ 833b93e83349
Branch filter:

Location: kallithea/kallithea/lib/hooks.py

Thomas De Schampheleire
hooks: add extensible create-pullrequest hook

Add a hook that will be called when a new pull request is created, and which
can be implemented in the 'extensions' package.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
kallithea.lib.hooks
~~~~~~~~~~~~~~~~~~~

Hooks run by Kallithea

This file was forked by the Kallithea project in July 2014.
Original author and date, and relevant copyright and licensing information is below:
:created_on: Aug 6, 2010
:author: marcink
:copyright: (c) 2013 RhodeCode GmbH, and others.
:license: GPLv3, see LICENSE.md for more details.
"""

import os
import sys
import time

import mercurial.scmutil

from kallithea.lib import helpers as h
from kallithea.lib.exceptions import UserCreationError
from kallithea.lib.utils import action_logger, make_ui
from kallithea.lib.utils2 import HookEnvironmentError, ascii_str, get_hook_environment, safe_bytes, safe_str
from kallithea.lib.vcs.backends.base import EmptyChangeset
from kallithea.model.db import Repository, User


def _get_scm_size(alias, root_path):
    if not alias.startswith('.'):
        alias += '.'

    size_scm, size_root = 0, 0
    for path, dirs, files in os.walk(root_path):
        if path.find(alias) != -1:
            for f in files:
                try:
                    size_scm += os.path.getsize(os.path.join(path, f))
                except OSError:
                    pass
        else:
            for f in files:
                try:
                    size_root += os.path.getsize(os.path.join(path, f))
                except OSError:
                    pass

    size_scm_f = h.format_byte_size(size_scm)
    size_root_f = h.format_byte_size(size_root)
    size_total_f = h.format_byte_size(size_root + size_scm)

    return size_scm_f, size_root_f, size_total_f


def repo_size(ui, repo, hooktype=None, **kwargs):
    """Show size of Mercurial repository.

    Called as Mercurial hook changegroup.repo_size after push.
    """
    size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', safe_str(repo.root))

    last_cs = repo[len(repo) - 1]

    msg = ('Repository size .hg: %s Checkout: %s Total: %s\n'
           'Last revision is now r%s:%s\n') % (
        size_hg_f, size_root_f, size_total_f, last_cs.rev(), ascii_str(last_cs.hex())[:12]
    )
    ui.status(safe_bytes(msg))


def log_pull_action(ui, repo, **kwargs):
    """Logs user last pull action

    Called as Mercurial hook outgoing.pull_logger or from Kallithea before invoking Git.

    Does *not* use the action from the hook environment but is always 'pull'.
    """
    ex = get_hook_environment()

    user = User.get_by_username(ex.username)
    action = 'pull'
    action_logger(user, action, ex.repository, ex.ip, commit=True)
    # extension hook call
    from kallithea import EXTENSIONS
    callback = getattr(EXTENSIONS, 'PULL_HOOK', None)
    if callable(callback):
        kw = {}
        kw.update(ex)
        callback(**kw)


def log_push_action(ui, repo, node, node_last, **kwargs):
    """
    Register that changes have been added to the repo - log the action *and* invalidate caches.
    Note: This hook is not only logging, but also the side effect invalidating
    caches! The function should perhaps be renamed.

    Called as Mercurial hook changegroup.kallithea_log_push_action .

    The pushed changesets is given by the revset 'node:node_last'.
    """
    revs = [ascii_str(repo[r].hex()) for r in mercurial.scmutil.revrange(repo, [b'%s:%s' % (node, node_last)])]
    process_pushed_raw_ids(revs)


def process_pushed_raw_ids(revs):
    """
    Register that changes have been added to the repo - log the action *and* invalidate caches.

    Called from Mercurial changegroup.kallithea_log_push_action calling hook log_push_action,
    or from the Git post-receive hook calling handle_git_post_receive ...
    or from scm _handle_push.
    """
    ex = get_hook_environment()

    action = '%s:%s' % (ex.action, ','.join(revs))
    action_logger(ex.username, action, ex.repository, ex.ip, commit=True)

    from kallithea.model.scm import ScmModel
    ScmModel().mark_for_invalidation(ex.repository)

    # extension hook call
    from kallithea import EXTENSIONS
    callback = getattr(EXTENSIONS, 'PUSH_HOOK', None)
    if callable(callback):
        kw = {'pushed_revs': revs}
        kw.update(ex)
        callback(**kw)


def log_create_repository(repository_dict, created_by, **kwargs):
    """
    Post create repository Hook.

    :param repository: dict dump of repository object
    :param created_by: username who created repository

    available keys of repository_dict:

     'repo_type',
     'description',
     'private',
     'created_on',
     'enable_downloads',
     'repo_id',
     'owner_id',
     'enable_statistics',
     'clone_uri',
     'fork_id',
     'group_id',
     'repo_name'

    """
    from kallithea import EXTENSIONS
    callback = getattr(EXTENSIONS, 'CREATE_REPO_HOOK', None)
    if callable(callback):
        kw = {}
        kw.update(repository_dict)
        kw.update({'created_by': created_by})
        kw.update(kwargs)
        callback(**kw)


def check_allowed_create_user(user_dict, created_by, **kwargs):
    # pre create hooks
    from kallithea import EXTENSIONS
    callback = getattr(EXTENSIONS, 'PRE_CREATE_USER_HOOK', None)
    if callable(callback):
        allowed, reason = callback(created_by=created_by, **user_dict)
        if not allowed:
            raise UserCreationError(reason)


def log_create_user(user_dict, created_by, **kwargs):
    """
    Post create user Hook.

    :param user_dict: dict dump of user object

    available keys for user_dict:

     'username',
     'full_name_or_username',
     'full_contact',
     'user_id',
     'name',
     'firstname',
     'short_contact',
     'admin',
     'lastname',
     'ip_addresses',
     'ldap_dn',
     'email',
     'api_key',
     'last_login',
     'full_name',
     'active',
     'password',
     'emails',

    """
    from kallithea import EXTENSIONS
    callback = getattr(EXTENSIONS, 'CREATE_USER_HOOK', None)
    if callable(callback):
        callback(created_by=created_by, **user_dict)


def log_create_pullrequest(pullrequest_dict, created_by, **kwargs):
    """
    Post create pull request hook.

    :param pullrequest_dict: dict dump of pull request object
    """
    from kallithea import EXTENSIONS
    callback = getattr(EXTENSIONS, 'CREATE_PULLREQUEST_HOOK', None)
    if callable(callback):
        return callback(created_by=created_by, **pullrequest_dict)

    return 0

def log_delete_repository(repository_dict, deleted_by, **kwargs):
    """
    Post delete repository Hook.

    :param repository: dict dump of repository object
    :param deleted_by: username who deleted the repository

    available keys of repository_dict:

     'repo_type',
     'description',
     'private',
     'created_on',
     'enable_downloads',
     'repo_id',
     'owner_id',
     'enable_statistics',
     'clone_uri',
     'fork_id',
     'group_id',
     'repo_name'

    """
    from kallithea import EXTENSIONS
    callback = getattr(EXTENSIONS, 'DELETE_REPO_HOOK', None)
    if callable(callback):
        kw = {}
        kw.update(repository_dict)
        kw.update({'deleted_by': deleted_by,
                   'deleted_on': time.time()})
        kw.update(kwargs)
        callback(**kw)


def log_delete_user(user_dict, deleted_by, **kwargs):
    """
    Post delete user Hook.

    :param user_dict: dict dump of user object

    available keys for user_dict:

     'username',
     'full_name_or_username',
     'full_contact',
     'user_id',
     'name',
     'firstname',
     'short_contact',
     'admin',
     'lastname',
     'ip_addresses',
     'ldap_dn',
     'email',
     'api_key',
     'last_login',
     'full_name',
     'active',
     'password',
     'emails',

    """
    from kallithea import EXTENSIONS
    callback = getattr(EXTENSIONS, 'DELETE_USER_HOOK', None)
    if callable(callback):
        callback(deleted_by=deleted_by, **user_dict)


def _hook_environment(repo_path):
    """
    Create a light-weight environment for stand-alone scripts and return an UI and the
    db repository.

    Git hooks are executed as subprocess of Git while Kallithea is waiting, and
    they thus need enough info to be able to create an app environment and
    connect to the database.
    """
    import paste.deploy

    import kallithea.config.application

    extras = get_hook_environment()

    path_to_ini_file = extras['config']
    config = paste.deploy.appconfig('config:' + path_to_ini_file)
    #logging.config.fileConfig(ini_file_path) # Note: we are in a different process - don't use configured logging
    kallithea.config.application.make_app(config.global_conf, **config.local_conf)

    # fix if it's not a bare repo
    if repo_path.endswith(os.sep + '.git'):
        repo_path = repo_path[:-5]

    repo = Repository.get_by_full_path(repo_path)
    if not repo:
        raise OSError('Repository %s not found in database' % repo_path)

    baseui = make_ui()
    return baseui, repo


def handle_git_pre_receive(repo_path, git_stdin_lines):
    """Called from Git pre-receive hook.
    The returned value is used as hook exit code and must be 0.
    """
    # Currently unused. TODO: remove?
    return 0


def handle_git_post_receive(repo_path, git_stdin_lines):
    """Called from Git post-receive hook.
    The returned value is used as hook exit code and must be 0.
    """
    try:
        baseui, repo = _hook_environment(repo_path)
    except HookEnvironmentError as e:
        sys.stderr.write("Skipping Kallithea Git post-recieve hook %r.\nGit was apparently not invoked by Kallithea: %s\n" % (sys.argv[0], e))
        return 0

    # the post push hook should never use the cached instance
    scm_repo = repo.scm_instance_no_cache()

    rev_data = []
    for l in git_stdin_lines:
        old_rev, new_rev, ref = l.strip().split(' ')
        _ref_data = ref.split('/')
        if _ref_data[1] in ['tags', 'heads']:
            rev_data.append({'old_rev': old_rev,
                             'new_rev': new_rev,
                             'ref': ref,
                             'type': _ref_data[1],
                             'name': '/'.join(_ref_data[2:])})

    git_revs = []
    for push_ref in rev_data:
        _type = push_ref['type']
        if _type == 'heads':
            if push_ref['old_rev'] == EmptyChangeset().raw_id:
                # update the symbolic ref if we push new repo
                if scm_repo.is_empty():
                    scm_repo._repo.refs.set_symbolic_ref(
                        b'HEAD',
                        b'refs/heads/%s' % safe_bytes(push_ref['name']))

                # build exclude list without the ref
                cmd = ['for-each-ref', '--format=%(refname)', 'refs/heads/*']
                stdout = scm_repo.run_git_command(cmd)
                ref = push_ref['ref']
                heads = [head for head in stdout.splitlines() if head != ref]
                # now list the git revs while excluding from the list
                cmd = ['log', push_ref['new_rev'], '--reverse', '--pretty=format:%H']
                cmd.append('--not')
                cmd.extend(heads) # empty list is ok
                stdout = scm_repo.run_git_command(cmd)
                git_revs += stdout.splitlines()

            elif push_ref['new_rev'] == EmptyChangeset().raw_id:
                # delete branch case
                git_revs += ['delete_branch=>%s' % push_ref['name']]
            else:
                cmd = ['log', '%(old_rev)s..%(new_rev)s' % push_ref,
                       '--reverse', '--pretty=format:%H']
                stdout = scm_repo.run_git_command(cmd)
                git_revs += stdout.splitlines()

        elif _type == 'tags':
            git_revs += ['tag=>%s' % push_ref['name']]

    process_pushed_raw_ids(git_revs)

    return 0


# Almost exactly like Mercurial contrib/hg-ssh:
def rejectpush(ui, **kwargs):
    """Mercurial hook to be installed as pretxnopen and prepushkey for read-only repos.
    Return value 1 will make the hook fail and reject the push.
    """
    ex = get_hook_environment()
    ui.warn(safe_bytes("Push access to %r denied\n" % ex.repository))
    return 1