blob: 556a9957d005d9aaa01480ac27367fb6e69bb754 [file] [log] [blame]
# Copyright 2025 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import collections
import logging
import re
import subprocess
# CSV-file like separators. The templating language doesn't support escaping,
# so we use
# https://en.wikipedia.org/wiki/C0_and_C1_control_codes#Field_separators
_NEWLINE = '\x1e'
_COMMA = '\x1f'
_TRAILER = re.compile(r'([a-zA-Z0-9\-_]+): (.*)')
def run_command(args: list[str],
check=True,
**kwargs) -> subprocess.CompletedProcess:
logging.debug('Running command %s', ' '.join(map(str, args)))
ps = subprocess.run(args, **kwargs, check=False)
if check and ps.returncode:
# Don't create a stack trace.
exit(ps.returncode)
return ps
def run_jj(args: list[str],
ignore_working_copy=False,
**kwargs) -> subprocess.CompletedProcess:
prefix = ['jj', '--no-pager']
if ignore_working_copy:
prefix.append('--ignore-working-copy')
return run_command(prefix + args, **kwargs)
def _log(args: list[str], templates: dict[str, str],
**kwargs) -> list[dict[str, str]]:
"""Log acts akin to a database query on a table.
The user will provide templates such as {
'change_id': 'change_id',
'parents': 'parents.map(|c| c.change_id())',
}
And a set of revisions to lookup (eg. 'a|b').
And it would then return [
{'change_id': 'a', 'parents': '<parent of a's id>'}
{'change_id': 'b', 'parents': '<parent of b's id>'}
]
"""
# Start by assigning indexes based on the field name.
fields, templates = zip(*sorted(templates.items()))
# We're just creating a jj template that outputs CSV files.
template = f' ++ "{_COMMA}" ++ '.join(templates)
template += f' ++ "{_NEWLINE}"'
stdout = run_jj(
[*args, '--no-graph', '-T', template],
stdout=subprocess.PIPE,
text=True,
**kwargs,
).stdout
# Now we parse our CSV file.
return [{
field: value
for field, value in zip(fields, change.split(_COMMA))
} for change in stdout.rstrip(_NEWLINE).split(_NEWLINE)]
def jj_log(*,
templates: dict[str, str],
revisions='@',
**kwargs) -> list[dict[str, str]]:
"""Retrieves information about jj revisions.
See _log for details."""
return _log(['log', '-r', revisions], templates, **kwargs)
def split_description(description: str) -> tuple[str, dict[str, list[str]]]:
"""Splits a description into the description and git trailers."""
trailers = collections.defaultdict(list)
user_desc, sep, trailer_paragraph = description.rstrip().rpartition('\n\n')
trailer_lines = trailer_paragraph.lstrip().split('\n')
# Note: for multiline values, we only retrieve the first line here.
for line in trailer_lines:
match = _TRAILER.match(line)
if match is not None:
trailers[match.group(1)].append(match.group(2))
if not trailers:
return description, {}
return user_desc, trailers