tinyhorse 0.1.2__tar.gz → 0.2.2__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.
- {tinyhorse-0.1.2/src/tinyhorse.egg-info → tinyhorse-0.2.2}/PKG-INFO +32 -4
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/README.md +31 -3
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/pyproject.toml +2 -1
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/src/tinyhorse/__init__.py +1 -1
- tinyhorse-0.2.2/src/tinyhorse/cli.py +347 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/src/tinyhorse/git.py +295 -2
- tinyhorse-0.2.2/src/tinyhorse/mcp.py +176 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/src/tinyhorse/workflow.py +179 -7
- {tinyhorse-0.1.2 → tinyhorse-0.2.2/src/tinyhorse.egg-info}/PKG-INFO +32 -4
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/src/tinyhorse.egg-info/SOURCES.txt +3 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/src/tinyhorse.egg-info/entry_points.txt +1 -0
- tinyhorse-0.2.2/tests/test_help_and_tag.py +101 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/tests/test_release_tool.py +1 -1
- tinyhorse-0.2.2/tests/test_wip.py +214 -0
- tinyhorse-0.1.2/src/tinyhorse/cli.py +0 -180
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/LICENSE +0 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/MANIFEST.in +0 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/docs/MVP.md +0 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/docs/RELEASING.md +0 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/docs/STORAGE_SAFETY.md +0 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/setup.cfg +0 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/src/tinyhorse/__main__.py +0 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/src/tinyhorse/auth.py +0 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/src/tinyhorse/config.py +0 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/src/tinyhorse/storage.py +0 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/src/tinyhorse.egg-info/dependency_links.txt +0 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/src/tinyhorse.egg-info/requires.txt +0 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/src/tinyhorse.egg-info/top_level.txt +0 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/tests/test_auth.py +0 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/tests/test_git.py +0 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/tests/test_storage.py +0 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/tests/test_workflow.py +0 -0
- {tinyhorse-0.1.2 → tinyhorse-0.2.2}/tools/release.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tinyhorse
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Git-native project control for local and user-owned infrastructure
|
|
5
5
|
Author: Tiny Horse contributors
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -87,11 +87,33 @@ tinyhorse projects
|
|
|
87
87
|
tinyhorse clone TULKAS
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
## Tiny verbs (0.2.2)
|
|
91
|
+
|
|
92
|
+
Autonomous git control uses short names. They wrap the same storage loop.
|
|
93
|
+
`tinyhorse --help` repeats this; every subcommand has a description.
|
|
94
|
+
|
|
95
|
+
| Tiny | Same as / meaning |
|
|
96
|
+
|------|-------------------|
|
|
97
|
+
| `tinyhorse park` | `push` — publish the canonical bundle, including parked WIP |
|
|
98
|
+
| `tinyhorse claim NAME` | `clone` — restore a Drive project by name |
|
|
99
|
+
| `tinyhorse doctor` | compare local vs canonical; dirty is ok if that dirt is parked |
|
|
100
|
+
| `tinyhorse freeze-writer` | refuse park/push from this checkout |
|
|
101
|
+
| `tinyhorse freeze-writer --clear` | unfreeze (or delete `.git/tinyhorse-freeze`) |
|
|
102
|
+
| `tinyhorse from-zip ZIP DEST` | GitHub zipball → **one new commit** (no history) |
|
|
103
|
+
| `tinyhorse ingest-github URL` | `migrate` a git URL → **full history** |
|
|
104
|
+
| `tinyhorse tag v1.2.0` | annotated git tag on **your** project, then park |
|
|
105
|
+
|
|
106
|
+
WIP (uncommitted files, extra worktrees, `refs/backup/`) is snapshotted into `refs/tinyhorse/wip/<name>` inside the **same** bundle. Park drops wip refs for worktrees that are gone or clean. An in-progress rebase/merge is refused until packed.
|
|
107
|
+
|
|
108
|
+
**Your versioning** is git tags on the project, not `tinyhorse --version`. `tinyhorse tag v1.0` creates `git tag -a v1.0` and parks so the tag lives in the canonical bundle.
|
|
109
|
+
|
|
110
|
+
MCP: `tinyhorse-mcp --help` then stdio JSON-RPC (`park`, `claim`, `doctor`, `from_zip`, `status`, `tag`).
|
|
111
|
+
|
|
90
112
|
`projects` discovers top-level Drive project folders that are marked by Tiny Horse
|
|
91
113
|
or contain Git bundles, surfaces empty/ambiguous folders instead of guessing, and
|
|
92
114
|
does not print provider folder/file IDs. `clone PROJECT` requires one exact project
|
|
93
115
|
name, restores the canonical bundle, writes the machine-local binding when needed,
|
|
94
|
-
and
|
|
116
|
+
and restores parked WIP (the tree may be dirty; that is success). Explicit `pull LOCATOR`
|
|
95
117
|
remains available for automation and unusual layouts.
|
|
96
118
|
|
|
97
119
|
The Drive adapter searches for exactly one canonical file with that name in that folder. `push` creates it once and then updates the same Drive object in place. Multiple same-named candidates are treated as an error instead of guessed around. Before replacing an existing object, Tiny Horse downloads and verifies canonical Git, compares its durable refs with the local bundle, and writes only with the snapshot token from that inspect. File storage serializes readers and writers with an exclusive lock, hashes the bytes it copies, and `fsync`s unique temporary files before replace. Drive content replacement uses Drive API v2 `If-Match` so a changed object returns HTTP 412 instead of a silent overwrite. It then reads the object back and requires the SHA-256 of the downloaded bytes to match the upload. See `docs/STORAGE_SAFETY.md`.
|
|
@@ -141,19 +163,25 @@ For an intentional history rewrite or ref deletion, use `tinyhorse push --force`
|
|
|
141
163
|
## CLI
|
|
142
164
|
|
|
143
165
|
```bash
|
|
166
|
+
tinyhorse --help
|
|
144
167
|
tinyhorse status [PATH]
|
|
145
168
|
tinyhorse init [PATH] (--drive | --storage LOCATOR) [--project NAME]
|
|
146
|
-
tinyhorse
|
|
169
|
+
tinyhorse park [PATH] [--force] # also: push
|
|
147
170
|
tinyhorse pull LOCATOR DESTINATION
|
|
148
171
|
tinyhorse doctor [PATH]
|
|
149
172
|
tinyhorse projects
|
|
150
|
-
tinyhorse
|
|
173
|
+
tinyhorse claim PROJECT [DESTINATION] # also: clone
|
|
174
|
+
tinyhorse tag NAME [PATH] [-m MSG] [--no-park]
|
|
175
|
+
tinyhorse freeze-writer [PATH] [--clear]
|
|
176
|
+
tinyhorse from-zip ZIP DEST [--project NAME] [--storage LOCATOR]
|
|
177
|
+
tinyhorse ingest-github URL [DEST] [--drive | --storage LOCATOR] [--project NAME]
|
|
151
178
|
tinyhorse bundle PATH OUTPUT
|
|
152
179
|
tinyhorse verify-bundle BUNDLE
|
|
153
180
|
tinyhorse migrate REMOTE [DESTINATION] [--drive | --storage LOCATOR] [--project NAME]
|
|
154
181
|
tinyhorse auth login [--client-secrets PATH] [--no-browser]
|
|
155
182
|
tinyhorse auth status [--refresh]
|
|
156
183
|
tinyhorse auth logout
|
|
184
|
+
tinyhorse-mcp --help
|
|
157
185
|
```
|
|
158
186
|
|
|
159
187
|
`status`, `migrate`, `push`, `pull`, `doctor`, and the auth commands produce machine-readable JSON where appropriate. `doctor` compares local HEAD to the canonical bundle and reports `in_sync`, `local_ahead`, `local_behind`, or `diverged_or_unrelated`. A refused safe push exits without replacing canonical storage and explains which HEAD, branch, tag, or ref would have been lost.
|
|
@@ -69,11 +69,33 @@ tinyhorse projects
|
|
|
69
69
|
tinyhorse clone TULKAS
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
## Tiny verbs (0.2.2)
|
|
73
|
+
|
|
74
|
+
Autonomous git control uses short names. They wrap the same storage loop.
|
|
75
|
+
`tinyhorse --help` repeats this; every subcommand has a description.
|
|
76
|
+
|
|
77
|
+
| Tiny | Same as / meaning |
|
|
78
|
+
|------|-------------------|
|
|
79
|
+
| `tinyhorse park` | `push` — publish the canonical bundle, including parked WIP |
|
|
80
|
+
| `tinyhorse claim NAME` | `clone` — restore a Drive project by name |
|
|
81
|
+
| `tinyhorse doctor` | compare local vs canonical; dirty is ok if that dirt is parked |
|
|
82
|
+
| `tinyhorse freeze-writer` | refuse park/push from this checkout |
|
|
83
|
+
| `tinyhorse freeze-writer --clear` | unfreeze (or delete `.git/tinyhorse-freeze`) |
|
|
84
|
+
| `tinyhorse from-zip ZIP DEST` | GitHub zipball → **one new commit** (no history) |
|
|
85
|
+
| `tinyhorse ingest-github URL` | `migrate` a git URL → **full history** |
|
|
86
|
+
| `tinyhorse tag v1.2.0` | annotated git tag on **your** project, then park |
|
|
87
|
+
|
|
88
|
+
WIP (uncommitted files, extra worktrees, `refs/backup/`) is snapshotted into `refs/tinyhorse/wip/<name>` inside the **same** bundle. Park drops wip refs for worktrees that are gone or clean. An in-progress rebase/merge is refused until packed.
|
|
89
|
+
|
|
90
|
+
**Your versioning** is git tags on the project, not `tinyhorse --version`. `tinyhorse tag v1.0` creates `git tag -a v1.0` and parks so the tag lives in the canonical bundle.
|
|
91
|
+
|
|
92
|
+
MCP: `tinyhorse-mcp --help` then stdio JSON-RPC (`park`, `claim`, `doctor`, `from_zip`, `status`, `tag`).
|
|
93
|
+
|
|
72
94
|
`projects` discovers top-level Drive project folders that are marked by Tiny Horse
|
|
73
95
|
or contain Git bundles, surfaces empty/ambiguous folders instead of guessing, and
|
|
74
96
|
does not print provider folder/file IDs. `clone PROJECT` requires one exact project
|
|
75
97
|
name, restores the canonical bundle, writes the machine-local binding when needed,
|
|
76
|
-
and
|
|
98
|
+
and restores parked WIP (the tree may be dirty; that is success). Explicit `pull LOCATOR`
|
|
77
99
|
remains available for automation and unusual layouts.
|
|
78
100
|
|
|
79
101
|
The Drive adapter searches for exactly one canonical file with that name in that folder. `push` creates it once and then updates the same Drive object in place. Multiple same-named candidates are treated as an error instead of guessed around. Before replacing an existing object, Tiny Horse downloads and verifies canonical Git, compares its durable refs with the local bundle, and writes only with the snapshot token from that inspect. File storage serializes readers and writers with an exclusive lock, hashes the bytes it copies, and `fsync`s unique temporary files before replace. Drive content replacement uses Drive API v2 `If-Match` so a changed object returns HTTP 412 instead of a silent overwrite. It then reads the object back and requires the SHA-256 of the downloaded bytes to match the upload. See `docs/STORAGE_SAFETY.md`.
|
|
@@ -123,19 +145,25 @@ For an intentional history rewrite or ref deletion, use `tinyhorse push --force`
|
|
|
123
145
|
## CLI
|
|
124
146
|
|
|
125
147
|
```bash
|
|
148
|
+
tinyhorse --help
|
|
126
149
|
tinyhorse status [PATH]
|
|
127
150
|
tinyhorse init [PATH] (--drive | --storage LOCATOR) [--project NAME]
|
|
128
|
-
tinyhorse
|
|
151
|
+
tinyhorse park [PATH] [--force] # also: push
|
|
129
152
|
tinyhorse pull LOCATOR DESTINATION
|
|
130
153
|
tinyhorse doctor [PATH]
|
|
131
154
|
tinyhorse projects
|
|
132
|
-
tinyhorse
|
|
155
|
+
tinyhorse claim PROJECT [DESTINATION] # also: clone
|
|
156
|
+
tinyhorse tag NAME [PATH] [-m MSG] [--no-park]
|
|
157
|
+
tinyhorse freeze-writer [PATH] [--clear]
|
|
158
|
+
tinyhorse from-zip ZIP DEST [--project NAME] [--storage LOCATOR]
|
|
159
|
+
tinyhorse ingest-github URL [DEST] [--drive | --storage LOCATOR] [--project NAME]
|
|
133
160
|
tinyhorse bundle PATH OUTPUT
|
|
134
161
|
tinyhorse verify-bundle BUNDLE
|
|
135
162
|
tinyhorse migrate REMOTE [DESTINATION] [--drive | --storage LOCATOR] [--project NAME]
|
|
136
163
|
tinyhorse auth login [--client-secrets PATH] [--no-browser]
|
|
137
164
|
tinyhorse auth status [--refresh]
|
|
138
165
|
tinyhorse auth logout
|
|
166
|
+
tinyhorse-mcp --help
|
|
139
167
|
```
|
|
140
168
|
|
|
141
169
|
`status`, `migrate`, `push`, `pull`, `doctor`, and the auth commands produce machine-readable JSON where appropriate. `doctor` compares local HEAD to the canonical bundle and reports `in_sync`, `local_ahead`, `local_behind`, or `diverged_or_unrelated`. A refused safe push exits without replacing canonical storage and explains which HEAD, branch, tag, or ref would have been lost.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tinyhorse"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Git-native project control for local and user-owned infrastructure"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -27,6 +27,7 @@ release = [
|
|
|
27
27
|
|
|
28
28
|
[project.scripts]
|
|
29
29
|
tinyhorse = "tinyhorse.cli:main"
|
|
30
|
+
tinyhorse-mcp = "tinyhorse.mcp:main"
|
|
30
31
|
|
|
31
32
|
[tool.setuptools.packages.find]
|
|
32
33
|
where = ["src"]
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from argparse import RawDescriptionHelpFormatter
|
|
6
|
+
|
|
7
|
+
from . import __version__
|
|
8
|
+
|
|
9
|
+
from .auth import AuthError, google_drive_auth_status, login_google_drive, logout_google_drive
|
|
10
|
+
from .config import ConfigError
|
|
11
|
+
from .git import GitError, create_bundle, repository_state, verify_bundle
|
|
12
|
+
from .storage import StorageError
|
|
13
|
+
from .workflow import (
|
|
14
|
+
WorkflowError,
|
|
15
|
+
clone_project,
|
|
16
|
+
doctor,
|
|
17
|
+
freeze_writer,
|
|
18
|
+
from_zip,
|
|
19
|
+
ingest_github,
|
|
20
|
+
init_drive_project,
|
|
21
|
+
init_project,
|
|
22
|
+
migrate_remote,
|
|
23
|
+
projects,
|
|
24
|
+
pull,
|
|
25
|
+
push,
|
|
26
|
+
tag_project,
|
|
27
|
+
unfreeze_writer,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
EPILOG = """
|
|
32
|
+
WIP: park snapshots dirty files, untracked files, and extra worktrees into
|
|
33
|
+
refs/tinyhorse/wip/<name> inside the same canonical bundle. refs/backup/* is
|
|
34
|
+
kept. An in-progress rebase/merge/cherry-pick is refused until finished.
|
|
35
|
+
Removed or clean worktrees drop their wip refs on the next park.
|
|
36
|
+
|
|
37
|
+
Your versioning: git tags on THIS repo are durable. Prefer:
|
|
38
|
+
tinyhorse tag v1.0.0
|
|
39
|
+
which creates an annotated git tag and parks. You can also `git tag -a` then
|
|
40
|
+
`tinyhorse park`. Tiny Horse's own package version (tinyhorse --version) is
|
|
41
|
+
unrelated to your project's tags.
|
|
42
|
+
|
|
43
|
+
Zip vs git URL:
|
|
44
|
+
from-zip GitHub zipball → ONE new commit (no history)
|
|
45
|
+
ingest-github / migrate clone the git URL → full history
|
|
46
|
+
|
|
47
|
+
Freeze: tinyhorse freeze-writer writes .git/tinyhorse-freeze.
|
|
48
|
+
Unfreeze: tinyhorse freeze-writer --clear (or delete that file).
|
|
49
|
+
|
|
50
|
+
MCP: tinyhorse-mcp --help then stdio JSON-RPC (park, claim, doctor, from_zip, status, tag).
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _parser() -> argparse.ArgumentParser:
|
|
55
|
+
parser = argparse.ArgumentParser(
|
|
56
|
+
prog="tinyhorse",
|
|
57
|
+
formatter_class=RawDescriptionHelpFormatter,
|
|
58
|
+
description="Git-native canonical storage. One bundle per project. Does not replace git.",
|
|
59
|
+
epilog=EPILOG,
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument("--version", action="version", version=f"tinyhorse {__version__}")
|
|
62
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
63
|
+
|
|
64
|
+
status = sub.add_parser(
|
|
65
|
+
"status",
|
|
66
|
+
help="print local Git state as JSON",
|
|
67
|
+
description="HEAD, branch, dirty, remotes. Does not talk to Drive.",
|
|
68
|
+
)
|
|
69
|
+
status.add_argument("path", nargs="?", default=".", help="git checkout (default: .)")
|
|
70
|
+
|
|
71
|
+
init = sub.add_parser(
|
|
72
|
+
"init",
|
|
73
|
+
help="bind an existing Git repo to canonical storage",
|
|
74
|
+
description="Writes .tinyhorse.toml (machine-local). Does not park yet.",
|
|
75
|
+
)
|
|
76
|
+
init.add_argument("path", nargs="?", default=".", help="git checkout (default: .)")
|
|
77
|
+
init_storage = init.add_mutually_exclusive_group(required=True)
|
|
78
|
+
init_storage.add_argument(
|
|
79
|
+
"--storage",
|
|
80
|
+
help="file:/path/to.bundle or gdrive://FOLDER_ID/FILE.bundle",
|
|
81
|
+
)
|
|
82
|
+
init_storage.add_argument(
|
|
83
|
+
"--drive",
|
|
84
|
+
action="store_true",
|
|
85
|
+
help="find or create a top-level Drive folder named --project (or the directory name)",
|
|
86
|
+
)
|
|
87
|
+
init.add_argument("--project", help="Drive/project display name; defaults to directory name")
|
|
88
|
+
|
|
89
|
+
force_help = (
|
|
90
|
+
"allow non-fast-forward history or ref deletion; "
|
|
91
|
+
"still aborts if storage changed during preflight"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
push_cmd = sub.add_parser(
|
|
95
|
+
"push",
|
|
96
|
+
help="safely publish a verified canonical bundle (also: park)",
|
|
97
|
+
description="Parks WIP into refs/tinyhorse/wip/*, then publishes. Dirty is included, not dropped.",
|
|
98
|
+
)
|
|
99
|
+
push_cmd.add_argument("path", nargs="?", default=".", help="git checkout (default: .)")
|
|
100
|
+
push_cmd.add_argument("--force", action="store_true", help=force_help)
|
|
101
|
+
|
|
102
|
+
pull_cmd = sub.add_parser(
|
|
103
|
+
"pull",
|
|
104
|
+
help="restore a repository from a storage locator",
|
|
105
|
+
description="Restores durable refs and parked WIP. Recovered trees may be dirty; that is success.",
|
|
106
|
+
)
|
|
107
|
+
pull_cmd.add_argument("storage", help="file:... or gdrive://FOLDER_ID/FILENAME")
|
|
108
|
+
pull_cmd.add_argument("destination", help="new directory; must not already exist")
|
|
109
|
+
|
|
110
|
+
doctor_cmd = sub.add_parser(
|
|
111
|
+
"doctor",
|
|
112
|
+
help="compare local state with canonical storage",
|
|
113
|
+
description=(
|
|
114
|
+
"ok if HEAD/refs match and either the tree is clean or its dirt is parked "
|
|
115
|
+
"in refs/tinyhorse/wip. Exit 1 if not ok."
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
doctor_cmd.add_argument("path", nargs="?", default=".", help="git checkout (default: .)")
|
|
119
|
+
|
|
120
|
+
sub.add_parser(
|
|
121
|
+
"projects",
|
|
122
|
+
help="list discoverable Tiny Horse projects on Google Drive",
|
|
123
|
+
description="Names and bundle filenames only; no Drive folder IDs.",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
clone_cmd = sub.add_parser(
|
|
127
|
+
"clone",
|
|
128
|
+
help="discover and restore a Tiny Horse project by name (also: claim)",
|
|
129
|
+
description="Restores WIP. Dirty after claim is ok when that dirt was parked.",
|
|
130
|
+
)
|
|
131
|
+
clone_cmd.add_argument("project", help="exact Drive project display name")
|
|
132
|
+
clone_cmd.add_argument("destination", nargs="?", help="new directory; defaults to the project name")
|
|
133
|
+
|
|
134
|
+
bundle = sub.add_parser("bundle", help="create a complete Git bundle (includes parked WIP)")
|
|
135
|
+
bundle.add_argument("path", help="git checkout")
|
|
136
|
+
bundle.add_argument("output", help="bundle file to write")
|
|
137
|
+
|
|
138
|
+
verify = sub.add_parser("verify-bundle", help="verify a Git bundle (header + fsck)")
|
|
139
|
+
verify.add_argument("bundle", help="path to a .bundle file")
|
|
140
|
+
|
|
141
|
+
park = sub.add_parser(
|
|
142
|
+
"park",
|
|
143
|
+
help="tiny: publish canonical bundle including WIP (same as push)",
|
|
144
|
+
description="Preferred autonomous name for push.",
|
|
145
|
+
)
|
|
146
|
+
park.add_argument("path", nargs="?", default=".", help="git checkout (default: .)")
|
|
147
|
+
park.add_argument("--force", action="store_true", help=force_help)
|
|
148
|
+
|
|
149
|
+
claim = sub.add_parser(
|
|
150
|
+
"claim",
|
|
151
|
+
help="tiny: restore a Drive project by name (same as clone)",
|
|
152
|
+
)
|
|
153
|
+
claim.add_argument("project", help="exact Drive project display name")
|
|
154
|
+
claim.add_argument("destination", nargs="?", help="new directory")
|
|
155
|
+
|
|
156
|
+
freeze = sub.add_parser(
|
|
157
|
+
"freeze-writer",
|
|
158
|
+
help="tiny: refuse park/push from this checkout until cleared",
|
|
159
|
+
description="Writes .git/tinyhorse-freeze. Unfreeze with --clear or by deleting that file.",
|
|
160
|
+
)
|
|
161
|
+
freeze.add_argument("path", nargs="?", default=".", help="git checkout (default: .)")
|
|
162
|
+
freeze.add_argument(
|
|
163
|
+
"--clear",
|
|
164
|
+
action="store_true",
|
|
165
|
+
help="remove .git/tinyhorse-freeze so park/push work again",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
fromzip = sub.add_parser(
|
|
169
|
+
"from-zip",
|
|
170
|
+
help="tiny: zipball → ONE new git commit (no history)",
|
|
171
|
+
description=(
|
|
172
|
+
"GitHub zipballs have no .git. Tiny Horse inits a repo and makes one commit. "
|
|
173
|
+
"For full history use ingest-github / migrate on the git URL. "
|
|
174
|
+
"Then init --drive and park, or pass --storage here."
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
fromzip.add_argument("archive", help="path to .zip (GitHub zipball or any project zip)")
|
|
178
|
+
fromzip.add_argument("destination", help="new directory; must not already exist")
|
|
179
|
+
fromzip.add_argument("--project", help="display name stored if --storage is also passed")
|
|
180
|
+
fromzip.add_argument(
|
|
181
|
+
"--storage",
|
|
182
|
+
help="optional bind: file:... or gdrive://FOLDER_ID/FILE.bundle",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
ingest = sub.add_parser(
|
|
186
|
+
"ingest-github",
|
|
187
|
+
help="tiny: git URL → Tiny Horse with full history (same as migrate)",
|
|
188
|
+
description="Does not modify the GitHub repo. Canonical authority becomes Tiny Horse storage.",
|
|
189
|
+
)
|
|
190
|
+
ingest.add_argument("remote", help="https://github.com/owner/repo.git or any git URL")
|
|
191
|
+
ingest.add_argument("destination", nargs="?", help="local checkout; defaults to repo name")
|
|
192
|
+
ingest.add_argument("--project", help="Drive/project display name")
|
|
193
|
+
ingest_storage = ingest.add_mutually_exclusive_group()
|
|
194
|
+
ingest_storage.add_argument("--drive", action="store_true", help="provision Drive (default if neither flag)")
|
|
195
|
+
ingest_storage.add_argument("--storage", help="file:... or gdrive:// locator")
|
|
196
|
+
|
|
197
|
+
tag = sub.add_parser(
|
|
198
|
+
"tag",
|
|
199
|
+
help="tiny: annotated git tag on THIS project, then park",
|
|
200
|
+
description=(
|
|
201
|
+
"Your versioning, not tinyhorse --version. "
|
|
202
|
+
"Creates git tag -a NAME and parks so the tag lives in the canonical bundle. "
|
|
203
|
+
"Example: tinyhorse tag v1.2.0 -m 'html-live S1 freeze'"
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
tag.add_argument("name", help="tag name, e.g. v1.2.0")
|
|
207
|
+
tag.add_argument("path", nargs="?", default=".", help="git checkout (default: .)")
|
|
208
|
+
tag.add_argument("-m", "--message", help="annotation; default: tinyhorse tag NAME")
|
|
209
|
+
tag.add_argument(
|
|
210
|
+
"--no-park",
|
|
211
|
+
action="store_true",
|
|
212
|
+
help="only create the git tag; do not park",
|
|
213
|
+
)
|
|
214
|
+
tag.add_argument("--force", action="store_true", help="move an existing tag, then park with --force")
|
|
215
|
+
|
|
216
|
+
move = sub.add_parser(
|
|
217
|
+
"migrate",
|
|
218
|
+
help="migrate a Git remote into Tiny Horse canonical storage",
|
|
219
|
+
description="Same as ingest-github. Never pushes to or deletes the source remote.",
|
|
220
|
+
)
|
|
221
|
+
move.add_argument("remote", help="git URL or local path")
|
|
222
|
+
move.add_argument(
|
|
223
|
+
"destination",
|
|
224
|
+
nargs="?",
|
|
225
|
+
help="local checkout destination; defaults to the repository name",
|
|
226
|
+
)
|
|
227
|
+
move.add_argument("--project", help="project display name; defaults to repository name")
|
|
228
|
+
migrate_storage = move.add_mutually_exclusive_group()
|
|
229
|
+
migrate_storage.add_argument(
|
|
230
|
+
"--drive",
|
|
231
|
+
action="store_true",
|
|
232
|
+
help="auto-provision Google Drive storage (the default)",
|
|
233
|
+
)
|
|
234
|
+
migrate_storage.add_argument(
|
|
235
|
+
"--storage",
|
|
236
|
+
help="explicit canonical storage locator instead of Google Drive",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
auth = sub.add_parser("auth", help="manage storage-provider authentication")
|
|
240
|
+
auth_sub = auth.add_subparsers(dest="auth_command", required=True)
|
|
241
|
+
|
|
242
|
+
auth_login = auth_sub.add_parser("login", help="sign in to Google Drive")
|
|
243
|
+
auth_login.add_argument(
|
|
244
|
+
"--client-secrets",
|
|
245
|
+
help="Google OAuth desktop-app client JSON; defaults to configured locations",
|
|
246
|
+
)
|
|
247
|
+
auth_login.add_argument(
|
|
248
|
+
"--no-browser",
|
|
249
|
+
action="store_true",
|
|
250
|
+
help="print the authorization URL instead of opening a browser",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
auth_status = auth_sub.add_parser("status", help="show Google Drive authentication state")
|
|
254
|
+
auth_status.add_argument(
|
|
255
|
+
"--refresh",
|
|
256
|
+
action="store_true",
|
|
257
|
+
help="refresh an expired access token while checking status",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
auth_sub.add_parser("logout", help="remove Tiny Horse's saved Google Drive credentials")
|
|
261
|
+
|
|
262
|
+
return parser
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def main(argv: list[str] | None = None) -> int:
|
|
266
|
+
args = _parser().parse_args(argv)
|
|
267
|
+
try:
|
|
268
|
+
if args.command == "status":
|
|
269
|
+
print(repository_state(args.path).to_json())
|
|
270
|
+
elif args.command == "init":
|
|
271
|
+
if args.drive:
|
|
272
|
+
print(init_drive_project(args.path, args.project))
|
|
273
|
+
else:
|
|
274
|
+
print(init_project(args.path, args.storage, args.project))
|
|
275
|
+
elif args.command in ("push", "park"):
|
|
276
|
+
print(push(args.path, force=args.force).to_json())
|
|
277
|
+
elif args.command == "pull":
|
|
278
|
+
print(pull(args.storage, args.destination).to_json())
|
|
279
|
+
elif args.command == "doctor":
|
|
280
|
+
result = doctor(args.path)
|
|
281
|
+
print(result.to_json())
|
|
282
|
+
return 0 if result.ok else 1
|
|
283
|
+
elif args.command == "projects":
|
|
284
|
+
print(projects().to_json())
|
|
285
|
+
elif args.command in ("clone", "claim"):
|
|
286
|
+
print(clone_project(args.project, args.destination).to_json())
|
|
287
|
+
elif args.command == "freeze-writer":
|
|
288
|
+
if args.clear:
|
|
289
|
+
print(str(unfreeze_writer(args.path)))
|
|
290
|
+
else:
|
|
291
|
+
print(str(freeze_writer(args.path)))
|
|
292
|
+
elif args.command == "from-zip":
|
|
293
|
+
print(from_zip(args.archive, args.destination, project=args.project, storage=args.storage))
|
|
294
|
+
elif args.command == "ingest-github":
|
|
295
|
+
print(
|
|
296
|
+
ingest_github(
|
|
297
|
+
args.remote,
|
|
298
|
+
args.destination,
|
|
299
|
+
project=args.project,
|
|
300
|
+
storage=args.storage,
|
|
301
|
+
).to_json()
|
|
302
|
+
)
|
|
303
|
+
elif args.command == "tag":
|
|
304
|
+
print(
|
|
305
|
+
tag_project(
|
|
306
|
+
args.path,
|
|
307
|
+
args.name,
|
|
308
|
+
message=args.message,
|
|
309
|
+
park_after=not args.no_park,
|
|
310
|
+
force=args.force,
|
|
311
|
+
).to_json()
|
|
312
|
+
)
|
|
313
|
+
elif args.command == "bundle":
|
|
314
|
+
print(create_bundle(args.path, args.output))
|
|
315
|
+
elif args.command == "verify-bundle":
|
|
316
|
+
print(verify_bundle(args.bundle))
|
|
317
|
+
elif args.command == "migrate":
|
|
318
|
+
print(
|
|
319
|
+
migrate_remote(
|
|
320
|
+
args.remote,
|
|
321
|
+
args.destination,
|
|
322
|
+
project=args.project,
|
|
323
|
+
storage=args.storage,
|
|
324
|
+
).to_json()
|
|
325
|
+
)
|
|
326
|
+
elif args.command == "auth":
|
|
327
|
+
if args.auth_command == "login":
|
|
328
|
+
print(
|
|
329
|
+
login_google_drive(
|
|
330
|
+
args.client_secrets,
|
|
331
|
+
open_browser=not args.no_browser,
|
|
332
|
+
).to_json()
|
|
333
|
+
)
|
|
334
|
+
elif args.auth_command == "status":
|
|
335
|
+
status = google_drive_auth_status(refresh=args.refresh)
|
|
336
|
+
print(status.to_json())
|
|
337
|
+
return 0 if status.authenticated else 1
|
|
338
|
+
elif args.auth_command == "logout":
|
|
339
|
+
print(logout_google_drive().to_json())
|
|
340
|
+
return 0
|
|
341
|
+
except (AuthError, GitError, ConfigError, StorageError, WorkflowError) as exc:
|
|
342
|
+
print(f"tinyhorse: {exc}", file=sys.stderr)
|
|
343
|
+
return 2
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
if __name__ == "__main__":
|
|
347
|
+
raise SystemExit(main())
|