Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/completions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Validate completions

on:
push:
branches:
- main
pull_request:
branches:
- main

permissions:
contents: read

jobs:
validate-completions:
timeout-minutes: 5
runs-on: ubuntu-24.04
name: validate-shell-completions
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install shells
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends zsh fish

- name: Run completion tests
run: bash tests/completions/test_completions.sh
10 changes: 8 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
.PHONY: help activate test lint install clean
.PHONY: help activate test test-completions lint install clean

# Default target
help:
@echo "ACP - Automatic Commit Pusher"
@echo ""
@echo "Available targets:"
@echo " make activate - Create venv and install dev dependencies"
@echo " make test - Run unit tests"
@echo " make test - Run unit tests"
@echo " make test-completions - Run shell completion tests in Docker"
@echo " make lint - Run linters/formatters"
@echo " make install - Install acp with pipx from current branch"
@echo " make clean - Clean up test artifacts"
Expand All @@ -22,6 +23,11 @@ activate:
test:
venv/bin/pytest test_acp.py -v

# Shell completion tests (Docker)
test-completions:
docker build -t acp-completions-test -f tests/completions/Dockerfile .
docker run --rm acp-completions-test

# Format and lint code
lint:
venv/bin/black .
Expand Down
168 changes: 168 additions & 0 deletions acp.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,155 @@
__version__ = "1.1.0"


def get_bash_completion():
"""Generate bash completion script."""
return """\
_acp() {
local cur prev commands opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
commands="pr checkout completions"
opts="-v --verbose -b --body -i --interactive --merge --auto-merge --merge-method -s --sync -a --add -r --reviewers -h --help --version"

if [[ ${COMP_CWORD} -eq 1 ]]; then
if [[ ${cur} == -* ]]; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
else
COMPREPLY=( $(compgen -W "${commands}" -- "${cur}") )
fi
return 0
fi

case "${prev}" in
--merge-method)
COMPREPLY=( $(compgen -W "merge squash rebase" -- "${cur}") )
return 0
;;
completions)
COMPREPLY=( $(compgen -W "bash zsh fish" -- "${cur}") )
return 0
;;
-b|--body|-r|--reviewers)
return 0
;;
esac

local subcmd="${COMP_WORDS[1]}"
if [[ ${cur} == -* ]] || [[ "${subcmd}" == "pr" && -z "${cur}" ]]; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
fi
}

complete -F _acp acp
"""


def get_zsh_completion():
"""Generate zsh completion script."""
return """\
#compdef acp

_acp() {
local curcontext="$curcontext" ret=1

_arguments -C \\
'1:command:->command' \\
'*::arg:->args' && ret=0

case $state in
command)
local -a commands
commands=(
'pr:Create a pull request from staged changes'
'checkout:Checkout a branch'
'completions:Output shell completion script'
)
_describe -t commands 'acp commands' commands && ret=0
;;
args)
case ${words[1]} in
pr)
_arguments \\
'1:commit message:' \\
'-v[Show detailed output]' \\
'--verbose[Show detailed output]' \\
'-b[Custom PR body message]:body:' \\
'--body[Custom PR body message]:body:' \\
'-i[Show PR creation URL]' \\
'--interactive[Show PR creation URL]' \\
'--merge[Merge PR immediately after creation]' \\
'--auto-merge[Enable auto-merge when checks pass]' \\
'--merge-method[Merge method]:method:(merge squash rebase)' \\
'-s[Sync current branch with remote after merge]' \\
'--sync[Sync current branch with remote after merge]' \\
'-a[Run git add . before committing]' \\
'--add[Run git add . before committing]' \\
'-r[Request reviewers]:reviewers:' \\
'--reviewers[Request reviewers]:reviewers:' \\
'-h[Show help]' \\
'--help[Show help]' && ret=0
;;
checkout)
_arguments '1:branch name:' && ret=0
;;
completions)
_arguments '1:shell:(bash zsh fish)' && ret=0
;;
esac
;;
esac

return ret
}

_acp "$@"
"""


def get_fish_completion():
"""Generate fish completion script."""
return """\
# Fish completion for acp

# Disable file completion by default
complete -c acp -f

# Commands
complete -c acp -n "__fish_use_subcommand" -a "pr" -d "Create a pull request from staged changes"
complete -c acp -n "__fish_use_subcommand" -a "checkout" -d "Checkout a branch"
complete -c acp -n "__fish_use_subcommand" -a "completions" -d "Output shell completion script"
complete -c acp -n "__fish_seen_subcommand_from completions" -a "bash zsh fish"

# Options
complete -c acp -s v -l verbose -d "Show detailed output"
complete -c acp -s b -l body -d "Custom PR body message" -r
complete -c acp -s i -l interactive -d "Show PR creation URL"
complete -c acp -l merge -d "Merge PR immediately after creation"
complete -c acp -l auto-merge -d "Enable auto-merge when checks pass"
complete -c acp -l merge-method -d "Merge method" -r -a "merge squash rebase"
complete -c acp -s s -l sync -d "Sync current branch with remote after merge"
complete -c acp -s a -l add -d "Run git add . before committing"
complete -c acp -s r -l reviewers -d "Comma-separated list of reviewers" -r
complete -c acp -s h -l help -d "Show help"
complete -c acp -l version -d "Show version"
"""


def print_completion(shell):
"""Print completion script for the specified shell."""
completions = {
"bash": get_bash_completion,
"zsh": get_zsh_completion,
"fish": get_fish_completion,
}
if shell not in completions:
print(f"Error: Unknown shell '{shell}'", file=sys.stderr)
print("Supported shells: bash, zsh, fish", file=sys.stderr)
sys.exit(1)
print(completions[shell]())


def run(cmd, quiet=False):
"""Run a command and return output."""
result = subprocess.run(cmd, capture_output=True, text=True)
Expand Down Expand Up @@ -493,12 +642,16 @@ def show_help():
"""Show help message."""
print("usage: acp pr <commit message> [pr options]")
print(" acp checkout <branch>")
print(" acp completions <shell>")
print()
print("Commands:")
print(" pr <message> Create a PR with staged changes")
print(
" checkout <branch> Checkout a branch, stripping 'user:' prefix if present"
)
print(
" completions <shell> Output shell completion script (bash, zsh, fish)"
)
print()
print("PR Options:")
print(" -a, --add Run 'git add .' before committing changes")
Expand All @@ -515,6 +668,12 @@ def show_help():
print(" --version Show version number")
print(" -h, --help Show this help message")
print()
print("Shell Completions:")
print(" Bash: echo 'eval \"$(acp completions bash)\"' >> ~/.bashrc")
print(" Zsh: echo 'eval \"$(acp completions zsh)\"' >> ~/.zshrc")
print(" Fish: acp completions fish | source")
print(" Or save to: ~/.config/fish/completions/acp.fish")
print()
print("Examples:")
print(' acp pr "fix: some typo" -i')
print(' acp pr "fix: urgent" -b "Closes issue #123" --merge --merge-method rebase')
Expand Down Expand Up @@ -788,6 +947,15 @@ def main():

args = parser.parse_args()

if args.command == "completions":
# args.message is the second positional arg (commit message for pr, branch for checkout)
shell = args.message
if not shell:
print("Error: Shell name required (bash, zsh, fish)", file=sys.stderr)
sys.exit(1)
print_completion(shell)
sys.exit(0)

if args.version:
print(f"acp version {__version__}")
sys.exit(0)
Expand Down
63 changes: 63 additions & 0 deletions test_acp.py
Original file line number Diff line number Diff line change
Expand Up @@ -1487,5 +1487,68 @@ def test_checkout_no_branch(self, capsys):
assert "Branch name required" in captured.err


class TestCompletion:
"""Test shell completion scripts."""

def test_completion_command_bash(self, capsys):
"""Test 'acp completions bash' outputs script."""
with mock.patch.object(sys, "argv", ["acp", "completions", "bash"]):
with pytest.raises(SystemExit) as exc:
acp.main()
assert exc.value.code == 0

captured = capsys.readouterr()
assert "_acp()" in captured.out
assert "complete -F _acp acp" in captured.out

def test_completion_command_zsh(self, capsys):
"""Test 'acp completions zsh' outputs script."""
with mock.patch.object(sys, "argv", ["acp", "completions", "zsh"]):
with pytest.raises(SystemExit) as exc:
acp.main()
assert exc.value.code == 0

captured = capsys.readouterr()
assert "#compdef acp" in captured.out
assert "_arguments" in captured.out

def test_completion_command_fish(self, capsys):
"""Test 'acp completions fish' outputs script."""
with mock.patch.object(sys, "argv", ["acp", "completions", "fish"]):
with pytest.raises(SystemExit) as exc:
acp.main()
assert exc.value.code == 0

captured = capsys.readouterr()
assert "complete -c acp" in captured.out

def test_completion_command_invalid(self, capsys):
"""Test 'acp completions invalid' with invalid shell."""
with mock.patch.object(sys, "argv", ["acp", "completions", "invalid"]):
with pytest.raises(SystemExit) as exc:
acp.main()
assert exc.value.code != 0

def test_completion_command_no_shell(self, capsys):
"""Test 'acp completions' without shell name."""
with mock.patch.object(sys, "argv", ["acp", "completions"]):
with pytest.raises(SystemExit) as exc:
acp.main()
assert exc.value.code != 0

captured = capsys.readouterr()
assert "Shell name required" in captured.err

def test_print_completion_invalid_shell(self, capsys):
"""Test print_completion() directly with invalid shell."""
with pytest.raises(SystemExit) as exc:
acp.print_completion("powershell")
assert exc.value.code == 1

captured = capsys.readouterr()
assert "Unknown shell" in captured.err
assert "Supported shells" in captured.err


if __name__ == "__main__":
pytest.main([__file__, "-v"])
16 changes: 16 additions & 0 deletions tests/completions/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM python:3.12-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
bash \
zsh \
fish \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY acp.py .
COPY tests/completions/test_completions.sh .

RUN chmod +x test_completions.sh

CMD ["./test_completions.sh"]
Loading