agit: init agit helper

This initializes a very simple AGit helper script.

Usage:

```
agit create
```
or
```
agit c
```

To create a new AGit Pr.

Integrate Pulling from an AGit remote.
Gitea doesn't expose an AGit endpoint in the api, or the `tea` cli.
This makes pulling not feasible, since there is no robust way to query
the AGit topic, which is the ref that need to be pulled.

One possible solution currently could be scraping the gitea instructions
for forking a pull request on an AGit PR.
This commit is contained in:
a-kenji
2025-06-10 12:34:20 +02:00
parent 03aa534ea4
commit 6b206deb72
5 changed files with 270 additions and 0 deletions

View File

@@ -33,6 +33,7 @@
self'.packages.tea-create-pr self'.packages.tea-create-pr
self'.packages.merge-after-ci self'.packages.merge-after-ci
self'.packages.pending-reviews self'.packages.pending-reviews
self'.packages.agit
# treefmt with config defined in ./flake-parts/formatting.nix # treefmt with config defined in ./flake-parts/formatting.nix
config.treefmt.build.wrapper 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, ... }: { config, pkgs, ... }:
{ {
packages = { packages = {
agit = pkgs.callPackage ./agit { };
tea-create-pr = pkgs.callPackage ./tea-create-pr { }; tea-create-pr = pkgs.callPackage ./tea-create-pr { };
zerotier-members = pkgs.callPackage ./zerotier-members { }; zerotier-members = pkgs.callPackage ./zerotier-members { };
zt-tcp-relay = pkgs.callPackage ./zt-tcp-relay { }; zt-tcp-relay = pkgs.callPackage ./zt-tcp-relay { };