Source code for git_well.git_autoconf_gpgsign

#!/usr/bin/env python
# PYTHON_ARGCOMPLETE_OK
from __future__ import annotations

from typing import Any

import scriptconfig as scfg
import ubelt as ub


[docs] class GitAutoconfGpgsignCLI(scfg.DataConfig): __command__ = 'autoconf-gpg' remote = scfg.Value(None, help='param1') repo_dpath = scfg.Value('.', help='repo to set gpg for') email = scfg.Value( None, help='The email the the signing GPG key is associated with. If unspecified attempt to infer via ssh credentials', )
[docs] @classmethod def main( cls, argv: list[str] | str | bool | None = True, **kwargs: Any ) -> None: """ Example: >>> # xdoctest: +SKIP >>> from git_well.git_autoconf_gpgsign import * # NOQA >>> argv = 0 >>> kwargs = dict() >>> cls = GitAutoconfGpgsignCLI >>> cls.main(argv=argv, **kwargs) """ import rich config = cls.cli(argv=argv, data=kwargs, strict=True) rich.print('config = ' + ub.urepr(config, nl=1)) from git_well.repo import Repo repo = Repo.coerce(config.repo_dpath) import os env = os.environ.copy() env['GIT_SSH_COMMAND'] = 'ssh -v' infos = [] for remote in repo.remotes: for url in list(remote.urls): print(f'url={url}') try: info = repo.cmd(f'git ls-remote {url}', env=env) except Exception: ... except KeyboardInterrupt: ... else: infos.append(info) identify_file_cands = ub.oset() for info in infos: for line in info.stderr.split('\n'): if 'identity file' in line: cand = line.split(' ')[3] cand = ub.Path(cand) if cand.exists(): identify_file_cands.add(cand) if config.email is None: email_candidates = ub.oset() for id_fpath in identify_file_cands: pub_key_fpath = id_fpath.augment(tail='.pub') if pub_key_fpath.exists(): pub_email = pub_key_fpath.read_text().strip().split(' ')[-1] email_candidates.add(pub_email) else: email_candidates = [config.email] gpg_candidates = [] for email in email_candidates: # info = ub.cmd(f'gpg --list-keys --keyid-format LONG "{email}"') # print(info.stdout) # entries = gpg_entries(email) candidates = lookup_gpg_keyinfos( email, verbose=0, allow_mainkey=False, capabilities='sign', mintrust='u', ) gpg_candidates.extend(candidates) unique_fprs = {cand['fpr'] for cand in gpg_candidates} if len(unique_fprs) == 0: print('Error') print(f'email_candidates = {ub.urepr(email_candidates, nl=1)}') print( f'identify_file_cands = {ub.urepr(identify_file_cands, nl=1)}' ) raise Exception( 'Unable to find any gpg candidates. ' 'Is the repo not using git/ssh credentials? ' 'Try specifying --email=<youremail>.' ) elif len(unique_fprs) != 1: print(f'unique_fprs = {ub.urepr(unique_fprs, nl=1)}') print('gpg_candidates = {}'.format(ub.urepr(gpg_candidates, nl=1))) raise AssertionError('need to choose 1') # assert len(gpg_candidates) == 1 fpr = ub.peek(unique_fprs) # https://help.github.com/en/articles/signing-commits repo.cmd('git config --local commit.gpgsign true') # Note the GPG key needs to match the email repo.cmd(f'git config --local user.email "{email}"') # Tell git which key to sign repo.cmd(f'git config --local user.signingkey "{fpr}"') print('CURRENT GLOBAL SETTINGS') repo.cmd( r'git config --global --list | grep "gpg\|sign\|email"', shell=True, verbose=1, ) print('CURRENT LOCAL SETTINGS') repo.cmd( r'git config --local --list | grep "gpg\|sign\|email"', shell=True, verbose=1, )
[docs] def _rich_print_records(records, title=None): """ Print a sequence of record dictionaries as a Rich table. This is intentionally small and local because it is only used for a debug-only display path. """ from rich.console import Console from rich.table import Table columns = [] seen = set() for record in records: for key in record: if key not in seen: seen.add(key) columns.append(key) table = Table(title=title) table.add_column('row', justify='right') for column in columns: table.add_column(str(column)) for idx, record in enumerate(records): table.add_row( str(idx), *[str(record.get(column, '')) for column in columns] ) Console().print(table)
[docs] def lookup_gpg_keyinfos( identifier, verbose=0, capabilities=None, allow_subkey=True, allow_mainkey=True, full=True, filter_expired=True, mintrust=None, ): """ Creates a table of information about GPG key candidates that match a query. Ignore: python ~/local/scripts/xgpg.py lookup_keyid "Emmy" python ~/local/scripts/xgpg.py lookup_keyid "Crall" --allow_mainkey=False --capabilities=sign python ~/local/scripts/xgpg.py lookup_keyid "Crall" --allow_mainkey=False --capabilities=encrypt python ~/local/scripts/xgpg.py lookup_keyid "Crall" --allow_mainkey=False --capabilities=auth python ~/local/scripts/xgpg.py lookup_keyid "Jonathan Crall" """ if capabilities is None: capabilities = {'certify'} if isinstance(capabilities, str): capabilities = set(capabilities.split(',')) entries = gpg_entries(identifier) # print(ub.urepr(entries, nl=2, sort=0)) if verbose: # print('entries = {}'.format(ub.urepr(entries, nl=2))) for entry_idx, rows in enumerate(entries): _rich_print_records(rows, title=f'GPG entry {entry_idx}') # print(ub.urepr(entries, nl=2, si=1, sort=0)) want_caps = {c[0] for c in capabilities} candidates = [] allowed_row_types = set() if allow_subkey: allowed_row_types.add('sub') if allow_mainkey: allowed_row_types.add('pub') for rows in entries: entry_uids = [] entry_candidates = [] trust = '-' for idx, row in enumerate(rows): row_ = {k: v for k, v in row.items() if v} if 'ownertrust' in row_: trust = row_['ownertrust'] if row['type'] == 'uid': entry_uids.append(row_) elif row['type'] in allowed_row_types: have_caps = set(row.get('capabilities', '')) if filter_expired: if row['valid'] == 'e': continue if have_caps.issuperset(want_caps): keyid = row['keyid'] if full: # Find the full fingerprint jdx = idx + 1 while jdx < len(rows) and rows[jdx]['type'] == 'fpr': fpr_row = rows[jdx] fpr = fpr_row['uid'] if fpr.endswith(keyid): keyid = fpr break jdx += 1 keyinfo = { 'fpr': fpr, 'trust': trust, 'trust_level': TRUST_CODE_TO_LEVEL[trust], **row_, } entry_candidates.append(keyinfo) for keyinfo in entry_candidates: keyinfo['uids'] = entry_uids candidates.extend(entry_candidates) if len(candidates) == 0: raise Exception('no matches found for this query') if mintrust: mintrust_level = TRUST_CODE_TO_LEVEL[mintrust] candidates = [ c for c in candidates if c.get('trust_level', 6) <= mintrust_level ] return candidates
[docs] def gpg_entries(identifier=None, verbose=0): """ References: # Format of the colon listings https://github.com/gpg/gnupg/blob/master/doc/DETAILS """ suffix = '' if identifier is not None: suffix = ' ' + chr(34) + identifier + chr(34) info = ub.cmd( 'gpg --with-colons --fixed-list-mode --list-keys --keyid-format LONG' + suffix, verbose=verbose, ) default_field_info = { 1: 'type', # Field 1 - Type of record 2: 'valid', # Field 2 - Validity 3: 'len', # Field 3 - Key length 4: 'pkalgo', # Field 4 - Public key algorithm 5: 'keyid', # Field 5 - KeyID 6: 'created', # Field 6 - Creation Date 7: 'expires', # Field 7 - Expiration Date 8: 'cert', 9: 'ownertrust', 10: 'uid', # Field 10 - UserId 11: 'sigclass', 12: 'capabilities', 13: 'issuer', 14: 'flags', 15: 'sn', 16: 'hasher', 17: 'curve', } special_info = { 'tru': {1: 'type', 2: 'stale', 3: 'trust', 4: 'date_create'}, 'pkd': {1: 'type', 2: 'index', 3: 'info', 4: 'value'}, 'cfg': {1: 'type'}, } header = [] entries = [] current = None valid_lines = [line for line in info['out'].split(chr(10)) if line] for line in valid_lines: parts = line.split(':') rec_type = parts[0] if rec_type in special_info: field_info = special_info[rec_type] else: field_info = default_field_info record = {} for i, val in enumerate(parts, start=1): record[field_info.get(i, i)] = val if record['type'] == 'pub': if current is not None: entries.append(current) current = [] if current is None: header.append(record) else: current.append(record) if current is not None: entries.append(current) return entries
TRUST_CODES = [ # Not sure if all levels are correct { 'code': '-', 'level': 4, 'desc': 'No ownertrust assigned / not yet calculated', }, { 'code': 'e', 'level': 4, 'desc': 'Trust calculation has failed; probably due to an expired key', }, {'code': 'q', 'level': 4, 'desc': 'Not enough information for calculation'}, {'code': 'n', 'level': 5, 'desc': 'Never trust this key'}, {'code': 'm', 'level': 2, 'desc': 'Marginally trusted'}, {'code': 'f', 'level': 1, 'desc': 'Fully trusted'}, {'code': 'u', 'level': 0, 'desc': 'Ultimately trusted'}, ] TRUST_CODE_TO_LEVEL = {d['code']: d['level'] for d in TRUST_CODES} __cli__ = GitAutoconfGpgsignCLI main = __cli__.main if __name__ == '__main__': """ CommandLine: python ~/code/git_well/git_well/git_autoconf_gpgsign.py python -m git_well.git_autoconf_gpgsign """ main()