Metadata-Version: 2.4
Name: tinyhorse
Version: 0.1.0
Summary: Git-native project control for local and user-owned infrastructure
Author: Tiny Horse contributors
License-Expression: Apache-2.0
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: gdrive
Requires-Dist: google-auth>=2.0; extra == "gdrive"
Requires-Dist: google-auth-oauthlib>=1.0; extra == "gdrive"
Provides-Extra: release
Requires-Dist: build>=1.2; extra == "release"
Requires-Dist: twine>=6; extra == "release"
Dynamic: license-file

# Tiny Horse

Tiny Horse is an open-source, Git-native control layer for projects that should remain usable on local or user-owned infrastructure.

It does **not** replace Git. It wraps standard Git operations with a small, stable interface that humans and agents can inspect without depending on GitHub as the source of truth.

## The storage loop

A Tiny Horse project has one canonical Git bundle on user-controlled storage. The repository contains a `.tinyhorse.toml` locator, not a mutable "current release" pointer. HEAD, refs, tags, timestamps, and history come from Git itself.

```bash
# Bind an existing repo to a local canonical object.
tinyhorse init . --storage file:/srv/git/my-project.bundle

# Safe by default: only fast-forwards canonical Git and preserves refs.
tinyhorse push
tinyhorse doctor

# Recover on another machine.
tinyhorse pull file:/srv/git/my-project.bundle ./my-project
```

## Google Drive login

Install the Drive extra once:

```bash
python -m pip install 'tinyhorse[gdrive]'
```

Tiny Horse uses Google's installed-app OAuth flow. It opens the browser, obtains a refresh token, and stores the credential **outside the repository**. You do not copy access tokens into shell variables for normal interactive use.

For development builds, provide a Google OAuth **Desktop app** client JSON once:

```bash
tinyhorse auth login --client-secrets ~/Downloads/client_secret.json
```

You can instead set `TINYHORSE_GOOGLE_CLIENT_SECRETS` or place that JSON at `~/.config/tinyhorse/google-client.json`. On first login Tiny Horse remembers the client configuration in its user config directory, so later logins do not need the path again. The resolver also supports a packaged `google-client.json`, allowing a first-party Tiny Horse OAuth client to remove this development-only step without changing project configuration.

```bash
tinyhorse auth status
# To force a refresh while checking:
tinyhorse auth status --refresh

tinyhorse auth logout
```

Saved refreshable credentials default to `~/.config/tinyhorse/google-drive-credentials.json` (or the platform/config override). The file is written with owner-only permissions where the OS supports them. `TINYHORSE_GOOGLE_DRIVE_TOKEN` is still accepted for CI and other intentionally non-interactive environments.

Then Drive setup no longer requires a folder ID or a `gdrive://` locator:

```bash
tinyhorse init . --drive
tinyhorse push
tinyhorse doctor
```

`init --drive` looks for one top-level Drive folder with the project name, creates
it if needed, adopts an existing single `.bundle` inside it when present, and
otherwise chooses a deterministic bundle filename. The resolved locator is stored
in `.tinyhorse.toml`; it is implementation detail rather than user input.

Explicit `gdrive://FOLDER_ID/FILENAME` locators remain supported for automation
and unusual layouts. Fresh-machine recovery no longer requires a locator. After authentication:

```bash
tinyhorse projects
tinyhorse clone TULKAS
```

`projects` discovers top-level Drive project folders that are marked by Tiny Horse
or contain Git bundles, surfaces empty/ambiguous folders instead of guessing, and
does not print provider folder/file IDs. `clone PROJECT` requires one exact project
name, restores the canonical bundle, writes the machine-local binding when needed,
and verifies that the restored working tree is clean. Explicit `pull LOCATOR`
remains available for automation and unusual layouts.

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 rechecks the provider version immediately before upload. It then reads the object back and verifies the result.


## One-command GitHub migration

Once Drive authentication is configured, a GitHub repository can be moved into
Tiny Horse canonical storage with one command:

```bash
tinyhorse migrate https://github.com/owner/repository.git
```

Tiny Horse derives the project and local checkout names from the remote, takes a
read-only mirror snapshot, materializes every source branch plus tags and Git notes
in a normal working checkout, auto-provisions the Drive project folder, safely
publishes the canonical bundle, downloads it again, and verifies that its durable
Git refs exactly match the source snapshot. The GitHub repository is never pushed
to, rewritten, deleted, archived, or otherwise modified by migration.

The original remote remains configured locally as `origin`, so it can still be
used as an optional legacy/read source. Canonical authority is the Tiny Horse
storage binding. For an exact-history import, the generated `.tinyhorse.toml` is
kept machine-local via `.git/info/exclude`; migration does not add a setup commit
or otherwise alter the imported Git history.

Useful overrides are available without changing the safe default:

```bash
# Pick a different checkout directory or display name.
tinyhorse migrate https://github.com/owner/repository.git ./repo --project "My Project"

# Use any supported user-controlled storage instead of Drive.
tinyhorse migrate https://github.com/owner/repository.git --storage file:/srv/git/repository.bundle
```

If the source already tracks a `.tinyhorse.toml`, migration refuses to replace
that existing binding implicitly.

## Safe push

Normal `tinyhorse push` is deliberately conservative. Canonical branches may only move forward, canonical tags and other durable refs may not be silently rewritten or deleted, and canonical HEAD may not be replaced by a stale or divergent local history. If another writer changes storage after Tiny Horse's preflight read, the push aborts rather than knowingly overwriting that newer state.

For an intentional history rewrite or ref deletion, use `tinyhorse push --force`. Force bypasses the Git ancestry/ref policy, **not** the storage-version check or readback verification. Remote-tracking refs (`refs/remotes/*`) are treated as caches and are no longer packaged as canonical project state.

## CLI

```bash
tinyhorse status [PATH]
tinyhorse init [PATH] (--drive | --storage LOCATOR) [--project NAME]
tinyhorse push [PATH] [--force]
tinyhorse pull LOCATOR DESTINATION
tinyhorse doctor [PATH]
tinyhorse projects
tinyhorse clone PROJECT [DESTINATION]
tinyhorse bundle PATH OUTPUT
tinyhorse verify-bundle BUNDLE
tinyhorse migrate REMOTE [DESTINATION] [--drive | --storage LOCATOR] [--project NAME]
tinyhorse auth login [--client-secrets PATH] [--no-browser]
tinyhorse auth status [--refresh]
tinyhorse auth logout
```

`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.

## Why TULKAS matters

TULKAS is the first proving case. It exposed a practical failure mode: when live Git, old GitHub snapshots, and point-in-time backups sit together, a fresh agent can guess wrong about what is current. Tiny Horse's rule is deliberately mechanical: one canonical Git object, archives elsewhere, and no hand-maintained freshness metadata.

## Package release gate

The repository includes a repeatable package release check and artifact builder:

```bash
python tools/release.py check
python tools/release.py build --allow-dev   # development candidate
```

Tiny Horse is licensed under Apache-2.0. A public release requires a final `X.Y.Z`
version, a matching `vX.Y.Z` Git tag, and package-index credentials. The build is
made from committed `HEAD`, verifies wheel/sdist metadata, smoke-installs the wheel
in a clean virtual environment, and writes SHA-256 hashes. See
`docs/RELEASING.md`.


## Development

```bash
python -m pip install -e .
python -m unittest discover -s tests -v
```
