def install_git_hooks(self, repo, force_create=False): """ Creates a kallithea hook inside a git repository :param repo: Instance of VCS repo :param force_create: Create even if same name hook exists """ loc = os.path.join(repo.path, 'hooks') if not repo.bare: loc = os.path.join(repo.path, '.git', 'hooks') if not os.path.isdir(loc): os.makedirs(loc) tmpl_post = b"#!%s\n" % safe_bytes(self._get_git_hook_interpreter()) tmpl_post += pkg_resources.resource_string( 'kallithea', os.path.join('config', 'post_receive_tmpl.py') ) tmpl_pre = b"#!%s\n" % safe_bytes(self._get_git_hook_interpreter()) tmpl_pre += pkg_resources.resource_string( 'kallithea', os.path.join('config', 'pre_receive_tmpl.py') ) for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]: _hook_file = os.path.join(loc, '%s-receive' % h_type) has_hook = False log.debug('Installing git hook in repo %s', repo) if os.path.exists(_hook_file): # let's take a look at this hook, maybe it's kallithea ? log.debug('hook exists, checking if it is from kallithea') with open(_hook_file, 'rb') as f: data = f.read() matches = re.search(br'^KALLITHEA_HOOK_VER\s*=\s*(.*)$', data, flags=re.MULTILINE) if matches: try: ver = matches.groups()[0] log.debug('Found Kallithea hook - it has KALLITHEA_HOOK_VER %r', ver) has_hook = True except Exception: log.error(traceback.format_exc()) else: # there is no hook in this dir, so we want to create one has_hook = True if has_hook or force_create: log.debug('writing %s hook file !', h_type) try: with open(_hook_file, 'wb') as f: tmpl = tmpl.replace(b'_TMPL_', safe_bytes(kallithea.__version__)) f.write(tmpl) os.chmod(_hook_file, 0o755) except IOError as e: log.error('error writing %s: %s', _hook_file, e) else: log.debug('skipping writing hook file')
def get_crypt_password(password): """ Cryptographic function used for bcrypt password hashing. :param password: password to hash """ return ascii_str(bcrypt.hashpw(safe_bytes(password), bcrypt.gensalt(10)))
def _make_app(self, parsed_request): """ Make an hgweb wsgi application. """ repo_name = parsed_request.repo_name repo_path = os.path.join(self.basepath, repo_name) baseui = make_ui(repo_path=repo_path) hgweb_app = mercurial.hgweb.hgweb(safe_bytes(repo_path), name=safe_bytes(repo_name), baseui=baseui) def wrapper_app(environ, start_response): environ['REPO_NAME'] = repo_name # used by mercurial.hgweb.hgweb return hgweb_app(environ, start_response) return wrapper_app
def __call__(self, environ, start_response): # Extract path_info as get_path_info does, but do it explicitly because # we also have to do the reverse operation when patching it back in path_info = safe_str(environ['PATH_INFO'].encode('latin1')) if path_info.startswith('/'): # it must path_info = '/' + fix_repo_id_name(path_info[1:]) environ['PATH_INFO'] = safe_bytes(path_info).decode('latin1') return self.application(environ, start_response)
def __get_lockkey(func, *fargs, **fkwargs): params = list(fargs) params.extend(['%s-%s' % ar for ar in fkwargs.items()]) func_name = str(func.__name__) if hasattr(func, '__name__') else str(func) lockkey = 'task_%s.lock' % \ md5(safe_bytes(func_name + '-' + '-'.join(str(x) for x in params))).hexdigest() return lockkey
def FID(raw_id, path): """ Creates a unique ID for filenode based on it's hash of path and revision it's safe to use in urls :param raw_id: :param path: """ return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_bytes(path)).hexdigest()[:12])
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 _request(self, url, body=None, headers=None, method=None, noformat=False, empty_response_ok=False): _headers = { "Content-type": "application/json", "Accept": "application/json" } if self.user and self.passwd: authstring = ascii_str( base64.b64encode(safe_bytes("%s:%s" % (self.user, self.passwd)))) _headers["Authorization"] = "Basic %s" % authstring if headers: _headers.update(headers) log.debug("Sent to crowd at %s:\nHeaders: %s\nBody:\n%s", url, _headers, body) req = urllib.request.Request(url, body, _headers) if method: req.get_method = lambda: method global msg msg = "" try: rdoc = self.opener.open(req) msg = "".join(rdoc.readlines()) if not msg and empty_response_ok: rval = {} rval["status"] = True rval["error"] = "Response body was empty" elif not noformat: rval = ext_json.loads(msg) rval["status"] = True else: rval = "".join(rdoc.readlines()) except Exception as e: if not noformat: rval = { "status": False, "body": body, "error": str(e) + "\n" + msg } else: rval = None return rval
def make_ui(repo_path=None): """ Create an Mercurial 'ui' object based on database Ui settings, possibly augmenting with content from a hgrc file. """ baseui = mercurial.ui.ui() # clean the baseui object baseui._ocfg = mercurial.config.config() baseui._ucfg = mercurial.config.config() baseui._tcfg = mercurial.config.config() sa = meta.Session() for ui_ in sa.query(Ui).order_by(Ui.ui_section, Ui.ui_key): if ui_.ui_active: log.debug('config from db: [%s] %s=%r', ui_.ui_section, ui_.ui_key, ui_.ui_value) baseui.setconfig( ascii_bytes(ui_.ui_section), ascii_bytes(ui_.ui_key), b'' if ui_.ui_value is None else safe_bytes(ui_.ui_value)) # force set push_ssl requirement to False, Kallithea handles that baseui.setconfig(b'web', b'push_ssl', False) baseui.setconfig(b'web', b'allow_push', b'*') # prevent interactive questions for ssh password / passphrase ssh = baseui.config(b'ui', b'ssh', default=b'ssh') baseui.setconfig(b'ui', b'ssh', b'%s -oBatchMode=yes -oIdentitiesOnly=yes' % ssh) # push / pull hooks baseui.setconfig(b'hooks', b'changegroup.kallithea_log_push_action', b'python:kallithea.lib.hooks.log_push_action') baseui.setconfig(b'hooks', b'outgoing.kallithea_log_pull_action', b'python:kallithea.lib.hooks.log_pull_action') if repo_path is not None: # Note: MercurialRepository / mercurial.localrepo.instance will do this too, so it will always be possible to override db settings or what is hardcoded above baseui.readconfig(repo_path) assert baseui.plain( ) # set by hgcompat.monkey_do (invoked from import of vcs.backends.hg) to minimize potential impact of loading config files return baseui
def gravatar_url(email_address, size=30, default=''): # doh, we need to re-import those to mock it later from kallithea.config.routing import url from kallithea.model.db import User from tg import tmpl_context as c if not c.visual.use_gravatar: return "" _def = '*****@*****.**' # default gravatar email_address = email_address or _def if email_address == _def: return default parsed_url = urllib.parse.urlparse(url.current(qualified=True)) url = (c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL) \ .replace('{email}', email_address) \ .replace('{md5email}', hashlib.md5(safe_bytes(email_address).lower()).hexdigest()) \ .replace('{netloc}', parsed_url.netloc) \ .replace('{scheme}', parsed_url.scheme) \ .replace('{size}', str(size)) return url
def check_password(password, hashed): """ Checks password match the hashed value using bcrypt. Remains backwards compatible and accept plain sha256 hashes which used to be used on Windows. :param password: password :param hashed: password in hashed form """ # sha256 hashes will always be 64 hex chars # bcrypt hashes will always contain $ (and be shorter) if len(hashed) == 64 and all(x in string.hexdigits for x in hashed): return hashlib.sha256(password).hexdigest() == hashed try: return bcrypt.checkpw(safe_bytes(password), ascii_bytes(hashed)) except ValueError as e: # bcrypt will throw ValueError 'Invalid hashed_password salt' on all password errors log.error('error from bcrypt checking password: %s', e) return False log.error('check_password failed - no method found for hash length %s', len(hashed)) return False
def _get_changesets(alias, org_repo, org_rev, other_repo, other_rev): """ Returns lists of changesets that can be merged from org_repo@org_rev to other_repo@other_rev ... and the other way ... and the ancestors that would be used for merge :param org_repo: repo object, that is most likely the original repo we forked from :param org_rev: the revision we want our compare to be made :param other_repo: repo object, most likely the fork of org_repo. It has all changesets that we need to obtain :param other_rev: revision we want out compare to be made on other_repo """ ancestors = None if org_rev == other_rev: org_changesets = [] other_changesets = [] elif alias == 'hg': # case two independent repos if org_repo != other_repo: hgrepo = mercurial.unionrepo.makeunionrepository( other_repo.baseui, safe_bytes(other_repo.path), safe_bytes(org_repo.path)) # all ancestors of other_rev will be in other_repo and # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot # no remote compare do it on the same repository else: hgrepo = other_repo._repo ancestors = [ ascii_str(hgrepo[ancestor].hex()) for ancestor in hgrepo.revs( b"id(%s) & ::id(%s)", ascii_bytes(other_rev), ascii_bytes(org_rev)) ] if ancestors: log.debug("shortcut found: %s is already an ancestor of %s", other_rev, org_rev) else: log.debug("no shortcut found: %s is not an ancestor of %s", other_rev, org_rev) ancestors = [ ascii_str(hgrepo[ancestor].hex()) for ancestor in hgrepo.revs(b"heads(::id(%s) & ::id(%s))", ascii_bytes(org_rev), ascii_bytes(other_rev)) ] # FIXME: expensive! other_changesets = [ other_repo.get_changeset(rev) for rev in hgrepo.revs( b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)", ascii_bytes(other_rev), ascii_bytes(org_rev), ascii_bytes(org_rev)) ] org_changesets = [ org_repo.get_changeset(ascii_str(hgrepo[rev].hex())) for rev in hgrepo.revs( b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)", ascii_bytes(org_rev), ascii_bytes(other_rev), ascii_bytes(other_rev)) ] elif alias == 'git': if org_repo != other_repo: from dulwich.repo import Repo from dulwich.client import SubprocessGitClient gitrepo = Repo(org_repo.path) SubprocessGitClient(thin_packs=False).fetch( other_repo.path, gitrepo) gitrepo_remote = Repo(other_repo.path) SubprocessGitClient(thin_packs=False).fetch( org_repo.path, gitrepo_remote) revs = [ ascii_str(x.commit.id) for x in gitrepo_remote.get_walker( include=[ascii_bytes(other_rev)], exclude=[ascii_bytes(org_rev)]) ] other_changesets = [ other_repo.get_changeset(rev) for rev in reversed(revs) ] if other_changesets: ancestors = [other_changesets[0].parents[0].raw_id] else: # no changesets from other repo, ancestor is the other_rev ancestors = [other_rev] gitrepo.close() gitrepo_remote.close() else: so = org_repo.run_git_command([ 'log', '--reverse', '--pretty=format:%H', '-s', '%s..%s' % (org_rev, other_rev) ]) other_changesets = [ org_repo.get_changeset(cs) for cs in re.findall(r'[0-9a-fA-F]{40}', so) ] so = org_repo.run_git_command( ['merge-base', org_rev, other_rev]) ancestors = [re.findall(r'[0-9a-fA-F]{40}', so)[0]] org_changesets = [] else: raise Exception('Bad alias only git and hg is allowed') return other_changesets, org_changesets, ancestors
def rejectpush(ui, **kwargs): """Mercurial hook to be installed as pretxnopen and prepushkey for read-only repos""" ex = get_hook_environment() ui.warn(safe_bytes("Push access to %r denied\n" % ex.repository)) return 1
def handle_git_post_receive(repo_path, git_stdin_lines): """Called from Git post-receive hook""" 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
def show(self, repo_name, pull_request_id, extra=None): c.pull_request = PullRequest.get_or_404(pull_request_id) c.allowed_to_change_status = self._is_allowed_to_change_status( c.pull_request) cc_model = ChangesetCommentsModel() cs_model = ChangesetStatusModel() # pull_requests repo_name we opened it against # ie. other_repo must match if repo_name != c.pull_request.other_repo.repo_name: raise HTTPNotFound # load compare data into template context c.cs_repo = c.pull_request.org_repo (c.cs_ref_type, c.cs_ref_name, c.cs_rev) = c.pull_request.org_ref.split(':') c.a_repo = c.pull_request.other_repo (c.a_ref_type, c.a_ref_name, c.a_rev) = c.pull_request.other_ref.split(':') # a_rev is ancestor org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!! c.cs_ranges = [] for x in c.pull_request.revisions: try: c.cs_ranges.append(org_scm_instance.get_changeset(x)) except ChangesetDoesNotExistError: c.cs_ranges = [] h.flash( _('Revision %s not found in %s') % (x, c.cs_repo.repo_name), 'error') break c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ... revs = [ctx.revision for ctx in reversed(c.cs_ranges)] c.jsdata = graph_data(org_scm_instance, revs) c.is_range = False try: if c.a_ref_type == 'rev': # this looks like a free range where target is ancestor cs_a = org_scm_instance.get_changeset(c.a_rev) root_parents = c.cs_ranges[0].parents c.is_range = cs_a in root_parents #c.merge_root = len(root_parents) > 1 # a range starting with a merge might deserve a warning except ChangesetDoesNotExistError: # probably because c.a_rev not found pass except IndexError: # probably because c.cs_ranges is empty, probably because revisions are missing pass avail_revs = set() avail_show = [] c.cs_branch_name = c.cs_ref_name c.a_branch_name = None other_scm_instance = c.a_repo.scm_instance c.update_msg = "" c.update_msg_other = "" try: if not c.cs_ranges: c.update_msg = _( 'Error: changesets not found when displaying pull request from %s.' ) % c.cs_rev elif org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor': if c.cs_ref_type != 'branch': c.cs_branch_name = org_scm_instance.get_changeset( c.cs_ref_name).branch # use ref_type ? c.a_branch_name = c.a_ref_name if c.a_ref_type != 'branch': try: c.a_branch_name = other_scm_instance.get_changeset( c.a_ref_name).branch # use ref_type ? except EmptyRepositoryError: c.a_branch_name = 'null' # not a branch name ... but close enough # candidates: descendants of old head that are on the right branch # and not are the old head itself ... # and nothing at all if old head is a descendant of target ref name if not c.is_range and other_scm_instance._repo.revs( 'present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name): c.update_msg = _( 'This pull request has already been merged to %s.' ) % c.a_branch_name elif c.pull_request.is_closed(): c.update_msg = _( 'This pull request has been closed and can not be updated.' ) else: # look for descendants of PR head on source branch in org repo avail_revs = org_scm_instance._repo.revs( '%s:: & branch(%s)', revs[0], c.cs_branch_name) if len(avail_revs) > 1: # more than just revs[0] # also show changesets that not are descendants but would be merged in targethead = other_scm_instance.get_changeset( c.a_branch_name).raw_id if org_scm_instance.path != other_scm_instance.path: # Note: org_scm_instance.path must come first so all # valid revision numbers are 100% org_scm compatible # - both for avail_revs and for revset results hgrepo = mercurial.unionrepo.makeunionrepository( org_scm_instance.baseui, safe_bytes(org_scm_instance.path), safe_bytes(other_scm_instance.path)) else: hgrepo = org_scm_instance._repo show = set( hgrepo.revs('::%ld & !::parents(%s) & !::%s', avail_revs, revs[0], targethead)) if show: c.update_msg = _( 'The following additional changes are available on %s:' ) % c.cs_branch_name else: c.update_msg = _( 'No additional changesets found for iterating on this pull request.' ) else: show = set() avail_revs = set() # drop revs[0] c.update_msg = _( 'No additional changesets found for iterating on this pull request.' ) # TODO: handle branch heads that not are tip-most brevs = org_scm_instance._repo.revs( '%s - %ld - %s', c.cs_branch_name, avail_revs, revs[0]) if brevs: # also show changesets that are on branch but neither ancestors nor descendants show.update( org_scm_instance._repo.revs( '::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name)) show.add( revs[0] ) # make sure graph shows this so we can see how they relate c.update_msg_other = _( 'Note: Branch %s has another head: %s.') % ( c.cs_branch_name, h.short_id( org_scm_instance.get_changeset( (max(brevs))).raw_id)) avail_show = sorted(show, reverse=True) elif org_scm_instance.alias == 'git': c.cs_repo.scm_instance.get_changeset( c.cs_rev ) # check it exists - raise ChangesetDoesNotExistError if not c.update_msg = _( "Git pull requests don't support iterating yet.") except ChangesetDoesNotExistError: c.update_msg = _( 'Error: some changesets not found when displaying pull request from %s.' ) % c.cs_rev c.avail_revs = avail_revs c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show] c.avail_jsdata = graph_data(org_scm_instance, avail_show) raw_ids = [x.raw_id for x in c.cs_ranges] c.cs_comments = c.cs_repo.get_comments(raw_ids) c.cs_statuses = c.cs_repo.statuses(raw_ids) ignore_whitespace = request.GET.get('ignorews') == '1' line_context = safe_int(request.GET.get('context'), 3) c.ignorews_url = _ignorews_url c.context_url = _context_url fulldiff = request.GET.get('fulldiff') diff_limit = None if fulldiff else self.cut_off_limit # we swap org/other ref since we run a simple diff on one repo log.debug('running diff between %s and %s in %s', c.a_rev, c.cs_rev, org_scm_instance.path) try: raw_diff = diffs.get_diff(org_scm_instance, rev1=c.a_rev, rev2=c.cs_rev, ignore_whitespace=ignore_whitespace, context=line_context) except ChangesetDoesNotExistError: raw_diff = safe_bytes( _("The diff can't be shown - the PR revisions could not be found." )) diff_processor = diffs.DiffProcessor(raw_diff, diff_limit=diff_limit) c.limited_diff = diff_processor.limited_diff c.file_diff_data = [] c.lines_added = 0 c.lines_deleted = 0 for f in diff_processor.parsed: st = f['stats'] c.lines_added += st['added'] c.lines_deleted += st['deleted'] filename = f['filename'] fid = h.FID('', filename) html_diff = diffs.as_html(enable_comments=True, parsed_lines=[f]) c.file_diff_data.append( (fid, None, f['operation'], f['old_filename'], filename, html_diff, st)) # inline comments c.inline_cnt = 0 c.inline_comments = cc_model.get_inline_comments( c.db_repo.repo_id, pull_request=pull_request_id) # count inline comments for __, lines in c.inline_comments: for comments in lines.values(): c.inline_cnt += len(comments) # comments c.comments = cc_model.get_comments(c.db_repo.repo_id, pull_request=pull_request_id) # (badly named) pull-request status calculation based on reviewer votes ( c.pull_request_reviewers, c.pull_request_pending_reviewers, c.current_voting_result, ) = cs_model.calculate_pull_request_result(c.pull_request) c.changeset_statuses = ChangesetStatus.STATUSES c.is_ajax_preview = False c.ancestors = None # [c.a_rev] ... but that is shown in an other way return render('/pullrequests/pullrequest_show.html')