Merge pull request 'agit: init agit helper' (#3938) from kenji/agit: init agit helper into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3938
This commit is contained in:
kenji
2025-06-11 10:39:48 +00:00
5 changed files with 270 additions and 0 deletions

View File

@@ -33,6 +33,7 @@
self'.packages.tea-create-pr
self'.packages.merge-after-ci
self'.packages.pending-reviews
self'.packages.agit
# treefmt with config defined in ./flake-parts/formatting.nix
config.treefmt.build.wrapper
];

37
pkgs/agit/README.md Normal file
View File

@@ -0,0 +1,37 @@
# agit
A helper script for the AGit workflow with a gitea instance.
<!-- `$ agit --help` -->
```
usage: agit [-h] {create,c} ...
AGit utility for creating and pulling PRs
positional arguments:
{create,c} Commands
create (c) Create an AGit PR
options:
-h, --help show this help message and exit
The defaults that are assumed are:
TARGET_REMOTE_REPOSITORY = origin
DEFAULT_TARGET_BRANCH = main
Examples:
$ agit create
Will create an AGit Pr with the latest commit message title as it's topic.
$ agit create --topic "my-feature"
Set a custom topic.
$ agit create --force
Force push to a certain topic
```
References:
- https://docs.gitea.com/usage/agit
- https://git-repo.info/en/2020/03/agit-flow-and-git-repo/

205
pkgs/agit/agit.py Normal file
View File

@@ -0,0 +1,205 @@
import argparse
import subprocess
import sys
# push origin HEAD:refs/for/main
# HEAD: The target branch
# origin: The target repository (not a fork!)
# HEAD: The local branch containing the changes you are proposing
TARGET_REMOTE_REPOSITORY = "origin"
DEFAULT_TARGET_BRANCH = "main"
def run_git_command(command: list) -> tuple[int, str, str]:
"""Run a git command and return exit code, stdout, and stderr."""
try:
result = subprocess.run(command, capture_output=True, text=True, check=False)
return result.returncode, result.stdout.strip(), result.stderr.strip()
except Exception as e:
return 1, "", str(e)
def get_latest_commit_info() -> tuple[str, str]:
"""Get the title and body of the latest commit."""
exit_code, commit_msg, error = run_git_command(
["git", "log", "-1", "--pretty=format:%B"]
)
if exit_code != 0:
print(f"Error getting commit info: {error}")
sys.exit(1)
lines = commit_msg.strip().split("\n")
title = lines[0].strip() if lines else ""
body_lines = []
for line in lines[1:]:
if body_lines or line.strip():
body_lines.append(line)
body = "\n".join(body_lines).strip()
return title, body
def create_agit_push(
remote: str = "origin",
branch: str = "main",
topic: str | None = None,
title: str | None = None,
description: str | None = None,
force_push: bool = False,
local_branch: str = "HEAD",
) -> None:
if topic is None or title is None:
commit_title, _ = get_latest_commit_info()
if topic is None:
topic = commit_title
if title is None:
title = commit_title
refspec = f"{local_branch}:refs/for/{branch}"
push_cmd = ["git", "push", remote, refspec]
push_cmd.extend(["-o", f"topic={topic}"])
if title:
push_cmd.extend(["-o", f"title={title}"])
if description:
escaped_desc = description.replace('"', '\\"')
push_cmd.extend(["-o", f"description={escaped_desc}"])
if force_push:
push_cmd.extend(["-o", "force-push"])
if description:
print(
f" Description: {description[:50]}..."
if len(description) > 50
else f" Description: {description}"
)
print()
exit_code, stdout, stderr = run_git_command(push_cmd)
if stdout:
print(stdout)
if stderr:
print(stderr, file=sys.stderr)
if exit_code != 0:
print("\nPush failed!")
sys.exit(exit_code)
else:
print("\nPush successful!")
def cmd_create(args: argparse.Namespace) -> None:
"""Handle the create subcommand."""
create_agit_push(
remote=args.remote,
branch=args.branch,
topic=args.topic,
title=args.title,
description=args.description,
force_push=args.force,
local_branch=args.local_branch,
)
def create_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="agit",
description="AGit utility for creating and pulling PRs",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=f"""
The defaults that are assumed are:
TARGET_REMOTE_REPOSITORY = {TARGET_REMOTE_REPOSITORY}
DEFAULT_TARGET_BRANCH = {DEFAULT_TARGET_BRANCH}
Examples:
$ agit create
Will create an AGit Pr with the latest commit message title as it's topic.
$ agit create --topic "my-feature"
Set a custom topic.
$ agit create --force
Force push to a certain topic
""",
)
subparsers = parser.add_subparsers(dest="subcommand", help="Commands")
create_parser = subparsers.add_parser(
"create",
aliases=["c"],
help="Create an AGit PR",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
$ agit create
Will create an AGit Pr with the latest commit message title as it's topic.
$ agit create --topic "my-feature"
Set a custom topic.
$ agit create --force
Force push to a certain topic
""",
)
create_parser.add_argument(
"-r",
"--remote",
default=TARGET_REMOTE_REPOSITORY,
help=f"Git remote to push to (default: {TARGET_REMOTE_REPOSITORY})",
)
create_parser.add_argument(
"-b",
"--branch",
default=DEFAULT_TARGET_BRANCH,
help=f"Target branch for the PR (default: {DEFAULT_TARGET_BRANCH})",
)
create_parser.add_argument(
"-l",
"--local-branch",
default="HEAD",
help="Local branch to push (default: HEAD)",
)
create_parser.add_argument(
"-t", "--topic", help="Set PR topic (default: last commit title)"
)
create_parser.add_argument(
"--title", help="Set the PR title (default: last commit title)"
)
create_parser.add_argument(
"--description", help="Override the PR description (default: commit body)"
)
create_parser.add_argument(
"-f", "--force", action="store_true", help="Force push the changes"
)
create_parser.set_defaults(func=cmd_create)
return parser
def main() -> None:
parser = create_parser()
args = parser.parse_args()
if args.subcommand is None:
parser.print_help()
sys.exit(0)
args.func(args)
if __name__ == "__main__":
main()

26
pkgs/agit/default.nix Normal file
View File

@@ -0,0 +1,26 @@
{
bash,
callPackage,
git,
lib,
openssh,
...
}:
let
writers = callPackage ../builders/script-writers.nix { };
in
writers.writePython3Bin "agit" {
flakeIgnore = [
"E501"
];
makeWrapperArgs = [
"--prefix"
"PATH"
":"
(lib.makeBinPath [
bash
git
openssh
])
];
} ./agit.py

View File

@@ -17,6 +17,7 @@
{ config, pkgs, ... }:
{
packages = {
agit = pkgs.callPackage ./agit { };
tea-create-pr = pkgs.callPackage ./tea-create-pr { };
zerotier-members = pkgs.callPackage ./zerotier-members { };
zt-tcp-relay = pkgs.callPackage ./zt-tcp-relay { };