docketeer-git 0.0.6__py3-none-any.whl
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.
docketeer_git/backup.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Git-backed workspace backup — periodic commit and optional push."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from docket.dependencies import Perpetual
|
|
10
|
+
|
|
11
|
+
from docketeer import environment
|
|
12
|
+
from docketeer.dependencies import EnvironmentStr, WorkspacePath
|
|
13
|
+
|
|
14
|
+
log = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
BACKUP_INTERVAL = environment.get_timedelta("GIT_BACKUP_INTERVAL", timedelta(minutes=5))
|
|
17
|
+
|
|
18
|
+
DEFAULT_GITIGNORE = """\
|
|
19
|
+
*.lock
|
|
20
|
+
*.tmp
|
|
21
|
+
*.swp
|
|
22
|
+
*~
|
|
23
|
+
__pycache__/
|
|
24
|
+
tmp/
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def _git(
|
|
29
|
+
cwd: Path,
|
|
30
|
+
*args: str,
|
|
31
|
+
env: dict[str, str] | None = None,
|
|
32
|
+
) -> asyncio.subprocess.Process:
|
|
33
|
+
"""Run a git command and return the completed process."""
|
|
34
|
+
proc = await asyncio.create_subprocess_exec(
|
|
35
|
+
"git",
|
|
36
|
+
*args,
|
|
37
|
+
cwd=cwd,
|
|
38
|
+
stdout=asyncio.subprocess.PIPE,
|
|
39
|
+
stderr=asyncio.subprocess.PIPE,
|
|
40
|
+
env={**os.environ, **env} if env else None,
|
|
41
|
+
)
|
|
42
|
+
await proc.wait()
|
|
43
|
+
return proc
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def _git_config(cwd: Path, key: str, value: str) -> None:
|
|
47
|
+
await _git(cwd, "config", key, value)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def _init_repo(
|
|
51
|
+
workspace: Path,
|
|
52
|
+
*,
|
|
53
|
+
branch: str,
|
|
54
|
+
remote: str,
|
|
55
|
+
author_name: str,
|
|
56
|
+
author_email: str,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Initialize a git repo in the workspace if one doesn't exist."""
|
|
59
|
+
await _git(workspace, "init", "-b", branch)
|
|
60
|
+
await _git_config(workspace, "user.name", author_name)
|
|
61
|
+
await _git_config(workspace, "user.email", author_email)
|
|
62
|
+
|
|
63
|
+
if remote:
|
|
64
|
+
await _git(workspace, "remote", "add", "origin", remote)
|
|
65
|
+
|
|
66
|
+
gitignore = workspace / ".gitignore"
|
|
67
|
+
if not gitignore.exists():
|
|
68
|
+
gitignore.write_text(DEFAULT_GITIGNORE)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def _has_changes(workspace: Path) -> bool:
|
|
72
|
+
"""Check if the workspace has any uncommitted changes or untracked files."""
|
|
73
|
+
status = await _git(workspace, "status", "--porcelain")
|
|
74
|
+
assert status.stdout is not None
|
|
75
|
+
output = await status.stdout.read()
|
|
76
|
+
return bool(output.strip())
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def backup(
|
|
80
|
+
perpetual: Perpetual = Perpetual(every=BACKUP_INTERVAL, automatic=True),
|
|
81
|
+
workspace: Path = WorkspacePath(),
|
|
82
|
+
remote: str = EnvironmentStr("GIT_REMOTE", ""),
|
|
83
|
+
branch: str = EnvironmentStr("GIT_BRANCH", "main"),
|
|
84
|
+
author_name: str = EnvironmentStr("GIT_AUTHOR_NAME", "Docketeer"),
|
|
85
|
+
author_email: str = EnvironmentStr("GIT_AUTHOR_EMAIL", "docketeer@localhost"),
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Commit workspace changes and optionally push to a remote."""
|
|
88
|
+
if not any(workspace.iterdir()):
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
if not (workspace / ".git").is_dir():
|
|
92
|
+
await _init_repo(
|
|
93
|
+
workspace,
|
|
94
|
+
branch=branch,
|
|
95
|
+
remote=remote,
|
|
96
|
+
author_name=author_name,
|
|
97
|
+
author_email=author_email,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if not await _has_changes(workspace):
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
await _git(workspace, "add", ".")
|
|
104
|
+
|
|
105
|
+
author_env = {
|
|
106
|
+
"GIT_AUTHOR_NAME": author_name,
|
|
107
|
+
"GIT_AUTHOR_EMAIL": author_email,
|
|
108
|
+
"GIT_COMMITTER_NAME": author_name,
|
|
109
|
+
"GIT_COMMITTER_EMAIL": author_email,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
timestamp = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S %z")
|
|
113
|
+
await _git(workspace, "commit", "-m", f"backup: {timestamp}", env=author_env)
|
|
114
|
+
log.info("Workspace backup committed: %s", timestamp)
|
|
115
|
+
|
|
116
|
+
if remote:
|
|
117
|
+
result = await _git(workspace, "push", "-u", "origin", branch)
|
|
118
|
+
if result.returncode != 0:
|
|
119
|
+
assert result.stderr is not None
|
|
120
|
+
stderr = (await result.stderr.read()).decode()
|
|
121
|
+
log.warning("Push failed: %s", stderr)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: docketeer-git
|
|
3
|
+
Version: 0.0.6
|
|
4
|
+
Summary: Git-backed workspace backup plugin for Docketeer
|
|
5
|
+
Project-URL: Homepage, https://github.com/chrisguidry/docketeer
|
|
6
|
+
Project-URL: Repository, https://github.com/chrisguidry/docketeer
|
|
7
|
+
Project-URL: Issues, https://github.com/chrisguidry/docketeer/issues
|
|
8
|
+
Author-email: Chris Guidry <guid@omg.lol>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
14
|
+
Classifier: Topic :: System :: Archiving :: Backup
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Requires-Dist: docketeer
|
|
17
|
+
Requires-Dist: pydocket
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# docketeer-git
|
|
21
|
+
|
|
22
|
+
Git-backed workspace backup plugin for
|
|
23
|
+
[Docketeer](https://pypi.org/project/docketeer/). Automatically commits the
|
|
24
|
+
agent's workspace (`~/.docketeer/memory/`) to a local git repo on a timer, and
|
|
25
|
+
optionally pushes to a remote for off-machine backup.
|
|
26
|
+
|
|
27
|
+
Install `docketeer-git` alongside `docketeer` and backups start automatically.
|
|
28
|
+
No agent-facing tools are added — the agent doesn't know about backups.
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
| Variable | Default | Description |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `DOCKETEER_GIT_BACKUP_INTERVAL` | `PT5M` | How often to check for changes (ISO 8601 duration or seconds) |
|
|
35
|
+
| `DOCKETEER_GIT_REMOTE` | _(empty)_ | Remote URL to push to. No push if unset. |
|
|
36
|
+
| `DOCKETEER_GIT_BRANCH` | `main` | Branch name to use |
|
|
37
|
+
| `DOCKETEER_GIT_AUTHOR_NAME` | `Docketeer` | Git author name for backup commits |
|
|
38
|
+
| `DOCKETEER_GIT_AUTHOR_EMAIL` | `docketeer@localhost` | Git author email for backup commits |
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
A periodic docket task checks the workspace for uncommitted changes every 5
|
|
43
|
+
minutes (configurable). If anything changed, it stages everything and commits
|
|
44
|
+
with a timestamped message. If `DOCKETEER_GIT_REMOTE` is set, it pushes after
|
|
45
|
+
each commit. Push failures are logged but don't crash the agent.
|
|
46
|
+
|
|
47
|
+
The git repo is initialized automatically on first run. You can browse the
|
|
48
|
+
history with standard git tools (`git log`, `git diff`, etc.) in the workspace
|
|
49
|
+
directory.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
docketeer_git/__init__.py,sha256=gB0tgUUEQcJuCOqG-_u91GieS_rsKWkeqNdHs43MBhw,155
|
|
2
|
+
docketeer_git/backup.py,sha256=wv2bR0xgdiCwlgW90LLjhs-srYXbUS2W-0oswQBJuzQ,3514
|
|
3
|
+
docketeer_git-0.0.6.dist-info/METADATA,sha256=rrsuqX4as8yZlBNnZoyQY97Kcl_V4v-F83DolM_Gse0,2155
|
|
4
|
+
docketeer_git-0.0.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
5
|
+
docketeer_git-0.0.6.dist-info/entry_points.txt,sha256=Hwuh47wBtr58oKO6mwasZtTKy7eIzxCMJl5BEsuF3aI,55
|
|
6
|
+
docketeer_git-0.0.6.dist-info/RECORD,,
|