Skip to content
Snippets Groups Projects
Commit 79bc61d2 authored by Hugo Hörnquist's avatar Hugo Hörnquist
Browse files

Add git-children.

parent aed4b93d
No related branches found
No related tags found
No related merge requests found
...@@ -4,5 +4,6 @@ PREFIX := /usr ...@@ -4,5 +4,6 @@ PREFIX := /usr
DESTDIR = DESTDIR =
install: install:
install git-children.py $(DESTDIR)$(PREFIX)/bin/git-children
install git-ls.scm $(DESTDIR)$(PREFIX)/bin/git-ls install git-ls.scm $(DESTDIR)$(PREFIX)/bin/git-ls
install git-open.py $(DESTDIR)$(PREFIX)/bin/git-open install git-open.py $(DESTDIR)$(PREFIX)/bin/git-open
#!/usr/bin/env python3
"""
Script for easily referencing children of a specific git commit.
Many of these procedures refer to the "current repo". This is simply
the closest containing repo from the current working directory (per
the git command lines rules).
The two types of move currently supported are:
- ``^[<n>]`` Move to the *nth* (default first) child of the node.
- ``~[<n>]`` Move *n* commits backwards, always chocing the first
child.
"""
import subprocess
import re
from typing import Literal, cast, Iterator, TypeAlias
import argparse
ChangeType: TypeAlias = Literal['^', '~']
Change: TypeAlias = tuple[ChangeType, int]
def children() -> dict[bytes, list[bytes]]:
"""Load list of all relations in the current git repo."""
cmd = subprocess.Popen("git rev-list --children --all".split(' '),
stdout=subprocess.PIPE)
tree: dict[bytes, list[bytes]] = {}
for line in cast(Iterator[bytes], cmd.stdout):
commit, *children = line.rstrip().split(b' ')
tree[commit] = children
return tree
def revparse(name: str) -> bytes:
"""Return the revparse of the name, in the current repo."""
return subprocess.run(["git", "rev-parse", name],
capture_output=True).stdout.rstrip()
def parse_refspec(spec: str) -> tuple[str, list[Change]]:
"""
Parse a git refspec.
Currently only basic syntax is supported, and is on the form
BASE^^
Where BASE is dircet reference to a commit (through a tag, branch,
or direct id), and `^^` is how we should move from that commit.
The exact same rules as for git-rev-parse(1) applies. (We just
interpret them in reverse).
https://git-scm.com/docs/revisions/
:return:
A tuple containing the base name, and a list containing each
change. For changes where no value was given, 1 is used.
"""
ms = list(re.finditer(r'([0-9]*)([~^])', ''.join(reversed(spec))))
changes: list[Change] = []
for m in ms:
changes.append((cast(ChangeType, m.group(2)),
int(m.group(1)) if m.group(1) else 1))
base: str
if ms:
base = ''.join(reversed(''.join(reversed(spec))[ms[-1].end():]))
else:
base = spec
return base, changes
def resolve_change_mods(
tree: dict[bytes, list[bytes]],
base: str,
changes: list[Change]) -> bytes:
"""
Resolve a refspec finding children.
This takes the output of ``parse_refspec``, and locates the wanted
commit.
:param tree:
Tree where the keys are commit ids, and the children that
commits childrens id.
:param base:
The commit to start the search from. Can be either a branch,
tag, id, or anything else git-rev-parse(1) handles.
:param change:
How we should move from that commit.
:returns:
The found commits id.
"""
node: bytes = revparse(base)
for (opt, count) in changes:
match opt:
case '^':
node = tree[node][count - 1]
case '~':
for _ in range(count):
node = tree[node][0]
return node
def build_parser():
"""Construct the command line argument parser."""
parser = argparse.ArgumentParser(
prog="git children",
description="Easily locale decendants of a git commit.")
parser.add_argument('revision',
action='store',
help="Target revision. Example: 'HEAD^^'.")
return parser
def main():
"""Entry point of program."""
args = build_parser().parse_args()
base, mods = parse_refspec(args.revision)
ref = resolve_change_mods(children(), base, mods)
print(ref.decode('ASCII'))
if __name__ == '__main__':
main()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment