giton 0.1.7__tar.gz → 0.1.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {giton-0.1.7 → giton-0.1.8}/.idea/workspace.xml +1 -3
- {giton-0.1.7 → giton-0.1.8}/CHANGELOG.md +12 -0
- {giton-0.1.7 → giton-0.1.8}/PKG-INFO +5 -5
- {giton-0.1.7 → giton-0.1.8}/README.md +4 -4
- giton-0.1.8/VERSION +1 -0
- giton-0.1.8/examples/plugins/README.md +46 -0
- giton-0.1.8/examples/plugins/test_plugin_lifecycle.py +116 -0
- {giton-0.1.7 → giton-0.1.8}/pyproject.toml +1 -1
- {giton-0.1.7 → giton-0.1.8}/src/giton/__init__.py +1 -1
- {giton-0.1.7 → giton-0.1.8}/src/giton/catalog.py +3 -3
- giton-0.1.8/tests/test_plugin_runner.py +208 -0
- {giton-0.1.7 → giton-0.1.8}/uv.lock +1 -1
- giton-0.1.7/VERSION +0 -1
- {giton-0.1.7 → giton-0.1.8}/.gitignore +0 -0
- {giton-0.1.7 → giton-0.1.8}/.idea/.gitignore +0 -0
- {giton-0.1.7 → giton-0.1.8}/.koru/event-store.jsonl +0 -0
- {giton-0.1.7 → giton-0.1.8}/.koru/events/observability.jsonl +0 -0
- {giton-0.1.7 → giton-0.1.8}/.koru/history.jsonl +0 -0
- {giton-0.1.7 → giton-0.1.8}/.koru/onboarding.json +0 -0
- {giton-0.1.7 → giton-0.1.8}/.koru/project.json +0 -0
- {giton-0.1.7 → giton-0.1.8}/.planfile/.koru/autonomous-state.json +0 -0
- {giton-0.1.7 → giton-0.1.8}/.planfile/.koru/autonomy-telemetry.json +0 -0
- {giton-0.1.7 → giton-0.1.8}/.planfile/.koru/event-store.jsonl +0 -0
- {giton-0.1.7 → giton-0.1.8}/.planfile/.koru/nfo-events.jsonl +0 -0
- {giton-0.1.7 → giton-0.1.8}/.planfile/.koru/operator-steps/mcp_koru.ticket +0 -0
- {giton-0.1.7 → giton-0.1.8}/.planfile/.koru/operator-steps/self_control.ticket +0 -0
- {giton-0.1.7 → giton-0.1.8}/.planfile/.koru/queue-runner.lock +0 -0
- {giton-0.1.7 → giton-0.1.8}/.planfile/.koru/serve-endpoint.json +0 -0
- {giton-0.1.7 → giton-0.1.8}/.planfile/config.yaml +0 -0
- {giton-0.1.7 → giton-0.1.8}/.planfile/sprints/current.yaml +0 -0
- {giton-0.1.7 → giton-0.1.8}/.planfile/sprints/current.yaml.lock +0 -0
- {giton-0.1.7 → giton-0.1.8}/LICENSE +0 -0
- {giton-0.1.7 → giton-0.1.8}/SUMD.md +0 -0
- {giton-0.1.7 → giton-0.1.8}/SUMR.md +0 -0
- {giton-0.1.7 → giton-0.1.8}/TODO.md +0 -0
- {giton-0.1.7 → giton-0.1.8}/app.doql.less +0 -0
- {giton-0.1.7 → giton-0.1.8}/examples/advanced/Dockerfile +0 -0
- {giton-0.1.7 → giton-0.1.8}/examples/advanced/README.md +0 -0
- {giton-0.1.7 → giton-0.1.8}/examples/advanced/logs/log.txt +0 -0
- {giton-0.1.7 → giton-0.1.8}/examples/advanced/main.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/examples/basic/Dockerfile +0 -0
- {giton-0.1.7 → giton-0.1.8}/examples/basic/README.md +0 -0
- {giton-0.1.7 → giton-0.1.8}/examples/basic/logs/log.txt +0 -0
- {giton-0.1.7 → giton-0.1.8}/examples/basic/main.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/examples/testing/Dockerfile +0 -0
- {giton-0.1.7 → giton-0.1.8}/examples/testing/README.md +0 -0
- {giton-0.1.7 → giton-0.1.8}/examples/testing/docker-compose.yml +0 -0
- {giton-0.1.7 → giton-0.1.8}/examples/testing/logs/log.txt +0 -0
- {giton-0.1.7 → giton-0.1.8}/examples/testing/sample_project/pyproject.toml +0 -0
- {giton-0.1.7 → giton-0.1.8}/examples/testing/sample_project/src/example.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/examples/testing/sample_project/tests/test_example.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/examples/testing/test_giton_integration.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/goal.yaml +0 -0
- {giton-0.1.7 → giton-0.1.8}/koru.yaml +0 -0
- {giton-0.1.7 → giton-0.1.8}/planfile.yaml +0 -0
- {giton-0.1.7 → giton-0.1.8}/prefact.yaml +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/README.md +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/analysis.toon.yaml +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/calls.mmd +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/calls.png +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/calls.toon.yaml +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/calls.yaml +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/compact_flow.mmd +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/compact_flow.png +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/context.md +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/duplication.toon.yaml +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/evolution.toon.yaml +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/flow.mmd +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/flow.png +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/index.html +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/logic.pl +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/map.toon.yaml +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/mermaid.export +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/planfile-tickets.yaml +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/project.toon.yaml +0 -0
- {giton-0.1.7 → giton-0.1.8}/project/prompt.txt +0 -0
- {giton-0.1.7 → giton-0.1.8}/project.sh +0 -0
- {giton-0.1.7 → giton-0.1.8}/src/giton/__main__.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/src/giton/cli.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/src/giton/config.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/src/giton/context.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/src/giton/history.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/src/giton/hooks.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/src/giton/interactive.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/src/giton/plugins.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/src/giton/policies.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/src/giton/repo_config.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/src/giton/runner.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/src/giton/shell.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/testql-scenarios/generated-cli-tests.testql.toon.yaml +0 -0
- {giton-0.1.7 → giton-0.1.8}/testql-scenarios/generated-from-pytests.testql.toon.yaml +0 -0
- {giton-0.1.7 → giton-0.1.8}/tests/__init__.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/tests/test_basic.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/tests/test_history.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/tests/test_policies.py +0 -0
- {giton-0.1.7 → giton-0.1.8}/tree.sh +0 -0
|
@@ -5,9 +5,7 @@
|
|
|
5
5
|
</component>
|
|
6
6
|
<component name="ChangeListManager">
|
|
7
7
|
<list default="true" id="9532a670-1f01-4d5e-9501-f3491f105c64" name="Changes" comment="">
|
|
8
|
-
<change afterPath="$PROJECT_DIR$/
|
|
9
|
-
<change afterPath="$PROJECT_DIR$/tree.sh" afterDir="false" />
|
|
10
|
-
<change beforePath="$PROJECT_DIR$/TODO.md" beforeDir="false" afterPath="$PROJECT_DIR$/TODO.md" afterDir="false" />
|
|
8
|
+
<change beforePath="$PROJECT_DIR$/src/giton/catalog.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/giton/catalog.py" afterDir="false" />
|
|
11
9
|
</list>
|
|
12
10
|
<option name="SHOW_DIALOG" value="false" />
|
|
13
11
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
@@ -54,6 +54,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
54
54
|
|
|
55
55
|
## [Unreleased]
|
|
56
56
|
|
|
57
|
+
## [0.1.8] - 2026-05-27
|
|
58
|
+
|
|
59
|
+
### Docs
|
|
60
|
+
- Update README.md
|
|
61
|
+
- Update examples/plugins/README.md
|
|
62
|
+
|
|
63
|
+
### Test
|
|
64
|
+
- Update tests/test_plugin_runner.py
|
|
65
|
+
|
|
66
|
+
### Other
|
|
67
|
+
- Update examples/plugins/test_plugin_lifecycle.py
|
|
68
|
+
|
|
57
69
|
## [0.1.7] - 2026-05-27
|
|
58
70
|
|
|
59
71
|
### Docs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: giton
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: Local AI layer for git: orchestrates policies & plugins between commit and push.
|
|
5
5
|
Author: semcod
|
|
6
6
|
Author-email: Tom Sapletta <tom@sapletta.com>
|
|
@@ -22,11 +22,11 @@ Description-Content-Type: text/markdown
|
|
|
22
22
|
|
|
23
23
|
## AI Cost Tracking
|
|
24
24
|
|
|
25
|
-
     
|
|
26
|
+
  
|
|
27
27
|
|
|
28
|
-
- 🤖 **LLM usage:** $0.
|
|
29
|
-
- 👤 **Human dev:** ~$
|
|
28
|
+
- 🤖 **LLM usage:** $0.9599 (11 commits)
|
|
29
|
+
- 👤 **Human dev:** ~$339 (3.4h @ $100/h, 30min dedup)
|
|
30
30
|
|
|
31
31
|
Generated on 2026-05-27 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
32
32
|
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
     
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $0.
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $0.9599 (11 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$339 (3.4h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
12
|
Generated on 2026-05-27 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
13
|
|
giton-0.1.8/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.8
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Plugin example
|
|
2
|
+
|
|
3
|
+
End-to-end demonstration of a custom giton plugin.
|
|
4
|
+
|
|
5
|
+
This example registers a tiny shell-script plugin (`my_lint`) that simply
|
|
6
|
+
echoes the staged paths it received. It then stages a file, runs the
|
|
7
|
+
`pre-commit` trigger, and asserts the plugin actually executed and the
|
|
8
|
+
`{paths}` placeholder was correctly expanded.
|
|
9
|
+
|
|
10
|
+
## Running
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# from the repo root
|
|
14
|
+
python -m pytest examples/plugins/test_plugin_lifecycle.py -v
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
You should see:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
PASSED test_register_run_and_unregister
|
|
21
|
+
PASSED test_default_catalog_pyqual_command_is_valid
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## What it covers
|
|
25
|
+
|
|
26
|
+
| Step | API used |
|
|
27
|
+
| ------------------------------ | ----------------------------------------------- |
|
|
28
|
+
| Build a fake plugin executable | plain `chmod +x` |
|
|
29
|
+
| Register a plugin record | `giton.config.upsert_plugin` |
|
|
30
|
+
| Run the hook trigger | `giton.runner.run_trigger("pre-commit")` |
|
|
31
|
+
| Inspect placeholder expansion | reading the file the fake plugin wrote |
|
|
32
|
+
| Unregister | `giton.config.remove_plugin` |
|
|
33
|
+
|
|
34
|
+
## Verifying the real default catalog
|
|
35
|
+
|
|
36
|
+
The catalog ships three default plugins (`pyqual`, `vallm`, `pretest`)
|
|
37
|
+
that point at sibling repos under `semcod/`. They are real Python
|
|
38
|
+
packages with real CLIs, so you can install one and run it for real:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
giton plugin install pyqual
|
|
42
|
+
giton hook pre-commit # runs `pyqual gates --workdir <repo>`
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
If `pyqual` reports a missing `pyqual.yaml`, run `pyqual init` first —
|
|
46
|
+
that's a `pyqual` configuration step, unrelated to giton.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""End-to-end example: register a custom plugin, run it, observe the result.
|
|
2
|
+
|
|
3
|
+
Run with:
|
|
4
|
+
pytest examples/plugins/test_plugin_lifecycle.py -v
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import stat
|
|
10
|
+
import subprocess
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _git(args, cwd):
|
|
17
|
+
return subprocess.run(
|
|
18
|
+
["git", *args], cwd=cwd, check=True, capture_output=True, text=True
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def isolated_repo(tmp_path, monkeypatch):
|
|
24
|
+
"""A fresh git repo + isolated XDG_CONFIG_HOME so we don't touch
|
|
25
|
+
the real user's giton plugins."""
|
|
26
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "xdg"))
|
|
27
|
+
import importlib
|
|
28
|
+
from giton import config as _cfg
|
|
29
|
+
importlib.reload(_cfg)
|
|
30
|
+
from giton import plugins as _pl, runner as _rn
|
|
31
|
+
importlib.reload(_pl)
|
|
32
|
+
importlib.reload(_rn)
|
|
33
|
+
|
|
34
|
+
repo = tmp_path / "repo"
|
|
35
|
+
repo.mkdir()
|
|
36
|
+
_git(["init", "-q"], repo)
|
|
37
|
+
_git(["config", "user.email", "demo@example.com"], repo)
|
|
38
|
+
_git(["config", "user.name", "demo"], repo)
|
|
39
|
+
(repo / "README.md").write_text("# demo\n")
|
|
40
|
+
_git(["add", "README.md"], repo)
|
|
41
|
+
_git(["commit", "-q", "-m", "chore: seed"], repo)
|
|
42
|
+
return repo
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_register_run_and_unregister(isolated_repo, tmp_path, monkeypatch):
|
|
46
|
+
# 1) build a fake plugin executable that records its argv to a log
|
|
47
|
+
bin_dir = tmp_path / "bin"
|
|
48
|
+
bin_dir.mkdir()
|
|
49
|
+
plugin = bin_dir / "my_lint"
|
|
50
|
+
log = tmp_path / "my_lint.log"
|
|
51
|
+
plugin.write_text(
|
|
52
|
+
"#!/bin/sh\n"
|
|
53
|
+
f"echo \"argv:$@\" > '{log}'\n"
|
|
54
|
+
"echo 'my_lint: ok'\n"
|
|
55
|
+
"exit 0\n"
|
|
56
|
+
)
|
|
57
|
+
plugin.chmod(plugin.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
58
|
+
monkeypatch.setenv("PATH", f"{bin_dir}:{os.environ['PATH']}")
|
|
59
|
+
|
|
60
|
+
# 2) register it as a giton plugin firing on pre-commit
|
|
61
|
+
from giton.config import PluginRecord, upsert_plugin, load_plugins, remove_plugin
|
|
62
|
+
upsert_plugin(PluginRecord(
|
|
63
|
+
name="my_lint",
|
|
64
|
+
description="example plugin",
|
|
65
|
+
category="lang:python",
|
|
66
|
+
exec_type="cli",
|
|
67
|
+
command="my_lint {paths}",
|
|
68
|
+
triggers=["pre-commit"],
|
|
69
|
+
install=None,
|
|
70
|
+
source="user",
|
|
71
|
+
enabled=True,
|
|
72
|
+
))
|
|
73
|
+
assert any(p.name == "my_lint" for p in load_plugins())
|
|
74
|
+
|
|
75
|
+
# 3) stage a python file and fire the pre-commit trigger
|
|
76
|
+
(isolated_repo / "module.py").write_text("def f():\n return 42\n")
|
|
77
|
+
_git(["add", "module.py"], isolated_repo)
|
|
78
|
+
|
|
79
|
+
from giton.runner import run_trigger
|
|
80
|
+
outcome = run_trigger("pre-commit", cwd=isolated_repo)
|
|
81
|
+
|
|
82
|
+
# 4) the plugin must have actually run
|
|
83
|
+
assert len(outcome.plugin_results) == 1
|
|
84
|
+
pr = outcome.plugin_results[0]
|
|
85
|
+
assert pr.plugin == "my_lint"
|
|
86
|
+
assert pr.ok, pr.stderr
|
|
87
|
+
assert "my_lint: ok" in pr.stdout
|
|
88
|
+
|
|
89
|
+
# 5) {paths} placeholder must have been expanded to the staged file
|
|
90
|
+
argv_line = log.read_text().strip()
|
|
91
|
+
assert "module.py" in argv_line, argv_line
|
|
92
|
+
|
|
93
|
+
# 6) unregister cleanly
|
|
94
|
+
assert remove_plugin("my_lint")
|
|
95
|
+
assert not any(p.name == "my_lint" for p in load_plugins())
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_default_catalog_pyqual_command_is_valid():
|
|
99
|
+
"""Smoke-test the catalog metadata for the three default plugins."""
|
|
100
|
+
from giton import catalog
|
|
101
|
+
|
|
102
|
+
expected = {"pyqual", "vallm", "pretest"}
|
|
103
|
+
actual = {e.record.name for e in catalog.defaults()}
|
|
104
|
+
assert expected == actual
|
|
105
|
+
|
|
106
|
+
for entry in catalog.defaults():
|
|
107
|
+
cmd = entry.record.command
|
|
108
|
+
# every default command must declare which binary to invoke and
|
|
109
|
+
# the binary name must equal the plugin name
|
|
110
|
+
first_token = cmd.split()[0]
|
|
111
|
+
assert first_token == entry.record.name, (
|
|
112
|
+
f"plugin {entry.record.name!r} command starts with "
|
|
113
|
+
f"{first_token!r}; commands should always invoke their own binary"
|
|
114
|
+
)
|
|
115
|
+
# placeholders must format cleanly with reasonable values
|
|
116
|
+
cmd.format(paths="a.py", diff_file="/tmp/d.patch", root="/tmp/r")
|
|
@@ -54,7 +54,7 @@ CATALOG: dict[str, CatalogEntry] = {
|
|
|
54
54
|
"Python code quality checks (lint, types, complexity).",
|
|
55
55
|
category="lang:python",
|
|
56
56
|
triggers=["pre-commit"],
|
|
57
|
-
command="pyqual
|
|
57
|
+
command="pyqual gates --workdir {root}",
|
|
58
58
|
pypi="pyqual",
|
|
59
59
|
local="../pyqual",
|
|
60
60
|
),
|
|
@@ -63,7 +63,7 @@ CATALOG: dict[str, CatalogEntry] = {
|
|
|
63
63
|
"Validate LLM-generated code/patches before they land in history.",
|
|
64
64
|
category="task:validate",
|
|
65
65
|
triggers=["post-commit"],
|
|
66
|
-
command="vallm
|
|
66
|
+
command="vallm batch {paths}",
|
|
67
67
|
pypi="vallm",
|
|
68
68
|
local="../vallm",
|
|
69
69
|
),
|
|
@@ -72,7 +72,7 @@ CATALOG: dict[str, CatalogEntry] = {
|
|
|
72
72
|
"Run / generate tests before push.",
|
|
73
73
|
category="task:test",
|
|
74
74
|
triggers=["pre-push"],
|
|
75
|
-
command="pretest
|
|
75
|
+
command="pretest scan",
|
|
76
76
|
pypi="pretest",
|
|
77
77
|
local="../pretest",
|
|
78
78
|
),
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""End-to-end tests for the plugin runner using a real fake-plugin script.
|
|
2
|
+
|
|
3
|
+
These tests verify that:
|
|
4
|
+
- `{paths}`, `{diff_file}`, `{root}` placeholders are correctly expanded
|
|
5
|
+
- Plugin stdout/stderr/exit-code are propagated into `TriggerOutcome`
|
|
6
|
+
- `enabled=False` plugins are skipped
|
|
7
|
+
- `unknown command` plugins are reported, not crashed
|
|
8
|
+
- `commit-msg` plugin trigger is honoured
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import stat
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
from giton.config import PluginRecord, USER_PLUGINS_FILE, load_plugins, save_plugins
|
|
20
|
+
from giton.runner import run_trigger
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _git(args, cwd):
|
|
24
|
+
return subprocess.run(
|
|
25
|
+
["git", *args], cwd=cwd, check=True, capture_output=True, text=True
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def repo(tmp_path, monkeypatch):
|
|
31
|
+
"""Initialise a clean repo + isolated giton config root."""
|
|
32
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "xdg"))
|
|
33
|
+
# reload config so USER_PLUGINS_FILE picks up the new XDG path
|
|
34
|
+
import importlib
|
|
35
|
+
from giton import config as _cfg
|
|
36
|
+
importlib.reload(_cfg)
|
|
37
|
+
from giton import plugins as _pl, runner as _rn
|
|
38
|
+
importlib.reload(_pl)
|
|
39
|
+
importlib.reload(_rn)
|
|
40
|
+
|
|
41
|
+
r = tmp_path / "r"
|
|
42
|
+
r.mkdir()
|
|
43
|
+
_git(["init", "-q"], r)
|
|
44
|
+
_git(["config", "user.email", "t@t"], r)
|
|
45
|
+
_git(["config", "user.name", "t"], r)
|
|
46
|
+
(r / "seed.txt").write_text("seed\n")
|
|
47
|
+
_git(["add", "seed.txt"], r)
|
|
48
|
+
_git(["commit", "-q", "-m", "feat: seed"], r)
|
|
49
|
+
return r
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _write_fake_plugin(tmp_path: Path, name: str = "fakeplugin") -> Path:
|
|
53
|
+
"""A real executable that records its argv and stdin to a file."""
|
|
54
|
+
script = tmp_path / name
|
|
55
|
+
log = tmp_path / f"{name}.log"
|
|
56
|
+
script.write_text(
|
|
57
|
+
"#!/bin/sh\n"
|
|
58
|
+
f"echo \"args:$@\" > '{log}'\n"
|
|
59
|
+
f"echo \"cwd:$PWD\" >> '{log}'\n"
|
|
60
|
+
"echo 'fake plugin ran'\n"
|
|
61
|
+
"exit ${FAKE_EXIT:-0}\n"
|
|
62
|
+
)
|
|
63
|
+
script.chmod(script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
64
|
+
return script
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _register(name: str, command: str, trigger: str, *, enabled: bool = True):
|
|
68
|
+
from giton import config as _cfg
|
|
69
|
+
plugins = _cfg.load_plugins()
|
|
70
|
+
plugins = [p for p in plugins if p.name != name]
|
|
71
|
+
plugins.append(PluginRecord(
|
|
72
|
+
name=name,
|
|
73
|
+
description="fake",
|
|
74
|
+
category="task:test",
|
|
75
|
+
exec_type="cli",
|
|
76
|
+
command=command,
|
|
77
|
+
triggers=[trigger],
|
|
78
|
+
install=None,
|
|
79
|
+
source="user",
|
|
80
|
+
enabled=enabled,
|
|
81
|
+
))
|
|
82
|
+
_cfg.save_plugins(plugins)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_plugin_executes_and_expands_paths(repo, tmp_path, monkeypatch):
|
|
86
|
+
fake = _write_fake_plugin(tmp_path)
|
|
87
|
+
log = tmp_path / "fakeplugin.log"
|
|
88
|
+
monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}")
|
|
89
|
+
_register("fakeplugin", "fakeplugin {paths}", "pre-commit")
|
|
90
|
+
|
|
91
|
+
# stage a couple of files
|
|
92
|
+
(repo / "a.py").write_text("a=1\n")
|
|
93
|
+
(repo / "b.py").write_text("b=2\n")
|
|
94
|
+
_git(["add", "a.py", "b.py"], repo)
|
|
95
|
+
|
|
96
|
+
out = run_trigger("pre-commit", cwd=repo)
|
|
97
|
+
assert len(out.plugin_results) == 1
|
|
98
|
+
pr = out.plugin_results[0]
|
|
99
|
+
assert pr.ok, pr.stderr
|
|
100
|
+
assert "fake plugin ran" in pr.stdout
|
|
101
|
+
# placeholder expansion: {paths} → "a.py b.py"
|
|
102
|
+
args_line = log.read_text().splitlines()[0]
|
|
103
|
+
assert "a.py" in args_line and "b.py" in args_line
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_plugin_diff_file_placeholder(repo, tmp_path, monkeypatch):
|
|
107
|
+
fake = _write_fake_plugin(tmp_path, "diffplug")
|
|
108
|
+
log = tmp_path / "diffplug.log"
|
|
109
|
+
monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}")
|
|
110
|
+
_register("diffplug", "diffplug --diff {diff_file}", "post-commit")
|
|
111
|
+
|
|
112
|
+
(repo / "x.py").write_text("x=1\n")
|
|
113
|
+
_git(["add", "x.py"], repo)
|
|
114
|
+
_git(["commit", "-q", "-m", "feat: x"], repo)
|
|
115
|
+
|
|
116
|
+
out = run_trigger("post-commit", cwd=repo)
|
|
117
|
+
assert out.plugin_results and out.plugin_results[0].ok
|
|
118
|
+
args_line = log.read_text().splitlines()[0]
|
|
119
|
+
assert "--diff" in args_line
|
|
120
|
+
# diff_file path always resolves under .giton/
|
|
121
|
+
assert ".giton" in args_line
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_plugin_root_placeholder(repo, tmp_path, monkeypatch):
|
|
125
|
+
fake = _write_fake_plugin(tmp_path, "rootplug")
|
|
126
|
+
log = tmp_path / "rootplug.log"
|
|
127
|
+
monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}")
|
|
128
|
+
_register("rootplug", "rootplug --workdir {root}", "pre-commit")
|
|
129
|
+
|
|
130
|
+
(repo / "z.txt").write_text("z\n")
|
|
131
|
+
_git(["add", "z.txt"], repo)
|
|
132
|
+
|
|
133
|
+
out = run_trigger("pre-commit", cwd=repo)
|
|
134
|
+
assert out.plugin_results[0].ok
|
|
135
|
+
args_line = log.read_text().splitlines()[0]
|
|
136
|
+
assert str(repo) in args_line
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_plugin_failure_propagates_to_outcome(repo, tmp_path, monkeypatch):
|
|
140
|
+
_write_fake_plugin(tmp_path, "failplug")
|
|
141
|
+
monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}")
|
|
142
|
+
monkeypatch.setenv("FAKE_EXIT", "3")
|
|
143
|
+
_register("failplug", "failplug", "pre-commit")
|
|
144
|
+
|
|
145
|
+
(repo / "f.txt").write_text("f\n")
|
|
146
|
+
_git(["add", "f.txt"], repo)
|
|
147
|
+
|
|
148
|
+
out = run_trigger("pre-commit", cwd=repo)
|
|
149
|
+
assert out.plugin_results[0].returncode == 3
|
|
150
|
+
assert not out.plugin_results[0].ok
|
|
151
|
+
assert not out.ok
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_plugin_disabled_is_skipped(repo, tmp_path, monkeypatch):
|
|
155
|
+
_write_fake_plugin(tmp_path, "skipplug")
|
|
156
|
+
monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}")
|
|
157
|
+
_register("skipplug", "skipplug", "pre-commit", enabled=False)
|
|
158
|
+
|
|
159
|
+
(repo / "g.txt").write_text("g\n")
|
|
160
|
+
_git(["add", "g.txt"], repo)
|
|
161
|
+
|
|
162
|
+
out = run_trigger("pre-commit", cwd=repo)
|
|
163
|
+
assert out.plugin_results == []
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_plugin_command_not_found(repo):
|
|
167
|
+
# do NOT alter PATH (git must remain available); use a binary name that
|
|
168
|
+
# is overwhelmingly unlikely to exist anywhere on the system.
|
|
169
|
+
_register(
|
|
170
|
+
"ghostplug", "giton-nonexistent-fake-binary-xyz123", "pre-commit"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
(repo / "h.txt").write_text("h\n")
|
|
174
|
+
_git(["add", "h.txt"], repo)
|
|
175
|
+
|
|
176
|
+
out = run_trigger("pre-commit", cwd=repo)
|
|
177
|
+
assert out.plugin_results[0].returncode == 127
|
|
178
|
+
assert not out.ok
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_multiple_plugins_run_in_order(repo, tmp_path, monkeypatch):
|
|
182
|
+
_write_fake_plugin(tmp_path, "p1")
|
|
183
|
+
_write_fake_plugin(tmp_path, "p2")
|
|
184
|
+
monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}")
|
|
185
|
+
_register("p1", "p1", "pre-commit")
|
|
186
|
+
_register("p2", "p2", "pre-commit")
|
|
187
|
+
|
|
188
|
+
(repo / "i.txt").write_text("i\n")
|
|
189
|
+
_git(["add", "i.txt"], repo)
|
|
190
|
+
|
|
191
|
+
out = run_trigger("pre-commit", cwd=repo)
|
|
192
|
+
names = [r.plugin for r in out.plugin_results]
|
|
193
|
+
assert names == ["p1", "p2"]
|
|
194
|
+
assert all(r.ok for r in out.plugin_results)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_default_catalog_commands_are_well_formed():
|
|
198
|
+
"""Catch regressions where a default plugin's command refers to a
|
|
199
|
+
non-existent subcommand. We can't run them in CI without the deps
|
|
200
|
+
installed, but we can verify the placeholder syntax parses."""
|
|
201
|
+
from giton import catalog
|
|
202
|
+
for entry in catalog.defaults():
|
|
203
|
+
cmd = entry.record.command
|
|
204
|
+
# placeholder substitution must succeed with reasonable inputs
|
|
205
|
+
formatted = cmd.format(paths="a.py", diff_file="/tmp/d.patch", root="/tmp/r")
|
|
206
|
+
assert formatted # non-empty
|
|
207
|
+
# and the binary name (first token) must equal the entry name
|
|
208
|
+
assert formatted.split()[0] == entry.record.name
|
giton-0.1.7/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.1.7
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|