gheasy
Install
pip install gheasy
Generate a CI workflow
uv_ci creates a complete Python CI workflow:
from gheasy.workflow import uv_ci
print(uv_ci("ci", lint_cmd=None).to_yaml())
name: ci
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup uv
uses: astral-sh/setup-uv@v5
- name: Install dependencies
run: uv sync --frozen
- name: Test
run: uv run pytest
Inspect a local repository
GitRepo.at accepts the repository root or any path inside it. brief returns a compact record for prompts and status lines. info returns the branches, remotes, changes, tags, stashes, active operation, filter health, and recovery points.
from gheasy.repo import GitRepo
repo = GitRepo.at('.')
status = repo.info()
status['branch'], status['clean'], status['ahead'], status['behind']
('claude/ramabana-branching-review-uqj6v2', False, 0, 0)
Preview before changing history
divergence compares the current branch with its upstream. It rehearses fast-forward, merge, rebase, and reset, then reports conflicts and a recommendation. merge_preview and rebase_preview inspect a specific integration without changing refs, the index, or the working tree.
preview = repo.divergence(fetch=True)
preview['relation'], preview['recommended'], preview['options']
repo.rebase_preview('origin/main')
{'current': 'claude/ramabana-branching-review-uqj6v2',
'onto': 'origin/main',
'clean': False,
'dirty': ['README.md',
'gheasy/core.py',
'gheasy/repo.py',
'gheasy/workflow.py',
'nbs/00_core.ipynb',
'nbs/02_repo.ipynb',
'nbs/index.ipynb',
'uv.lock'],
'untracked': 8,
'merge_base': '80bb02422f0bbe2ec6dcef2e74c641b19879ac16',
'commits': [{'oid': 'bc8bbb72c1003c9adcca87f3070cb8bdb8efb000',
'short': 'bc8bbb7',
'parents': ['ea046eeb5df51e5f0fac69276a1985bcbfa4f23e'],
'author': 'Claude',
'email': 'noreply@anthropic.com',
'timestamp': 1787690153,
'decoration': 'HEAD -> claude/ramabana-branching-review-uqj6v2, origin/claude/ramabana-branching-review-uqj6v2, origin/claude/ramabana-branching-notebook-nbnzye',
'signature': 'N',
'subject': 'Release 0.0.8'},
{'oid': 'ea046eeb5df51e5f0fac69276a1985bcbfa4f23e',
'short': 'ea046ee',
'parents': ['80bb02422f0bbe2ec6dcef2e74c641b19879ac16'],
'author': 'Claude',
'email': 'noreply@anthropic.com',
'timestamp': 1787690023,
'decoration': '',
'signature': 'N',
'subject': 'Add gheasy.repo: git repository operations as a literate notebook'}],
'conflicts': [],
'conflict_likely': False,
'already_based': True,
'review': {'base': 'origin/main',
'head': 'claude/ramabana-branching-review-uqj6v2',
'base_oid': '80bb02422f0bbe2ec6dcef2e74c641b19879ac16',
'head_oid': 'bc8bbb72c1003c9adcca87f3070cb8bdb8efb000',
'diff_base_oid': '80bb02422f0bbe2ec6dcef2e74c641b19879ac16',
'merge_base': '80bb02422f0bbe2ec6dcef2e74c641b19879ac16',
'mode': 'review',
'base_only': 0,
'head_only': 2,
'files': [{'path': 'CHANGELOG.md',
'old_path': '',
'status': 'M',
'similarity': 0,
'additions': 8,
'deletions': 0,
'binary': False},
{'path': 'gheasy/__init__.py',
'old_path': '',
'status': 'M',
'similarity': 0,
'additions': 1,
'deletions': 1,
'binary': False},
{'path': 'gheasy/_modidx.py',
'old_path': '',
'status': 'M',
'similarity': 0,
'additions': 152,
'deletions': 0,
'binary': False},
{'path': 'gheasy/core.py',
'old_path': '',
'status': 'M',
'similarity': 0,
'additions': 9,
'deletions': 7,
'binary': False},
{'path': 'gheasy/repo.py',
'old_path': '',
'status': 'A',
'similarity': 0,
'additions': 1659,
'deletions': 0,
'binary': False},
{'path': 'nbs/00_core.ipynb',
'old_path': '',
'status': 'M',
'similarity': 0,
'additions': 9,
'deletions': 7,
'binary': False},
{'path': 'nbs/02_repo.ipynb',
'old_path': '',
'status': 'A',
'similarity': 0,
'additions': 4298,
'deletions': 0,
'binary': False},
{'path': 'uv.lock',
'old_path': '',
'status': 'M',
'similarity': 0,
'additions': 1931,
'deletions': 2551,
'binary': False}],
'snapshot_alternative': None,
'commits': [{'oid': 'bc8bbb72c1003c9adcca87f3070cb8bdb8efb000',
'short': 'bc8bbb7',
'parents': ['ea046eeb5df51e5f0fac69276a1985bcbfa4f23e'],
'author': 'Claude',
'email': 'noreply@anthropic.com',
'timestamp': 1787690153,
'decoration': 'HEAD -> claude/ramabana-branching-review-uqj6v2, origin/claude/ramabana-branching-review-uqj6v2, origin/claude/ramabana-branching-notebook-nbnzye',
'signature': 'N',
'subject': 'Release 0.0.8'},
{'oid': 'ea046eeb5df51e5f0fac69276a1985bcbfa4f23e',
'short': 'ea046ee',
'parents': ['80bb02422f0bbe2ec6dcef2e74c641b19879ac16'],
'author': 'Claude',
'email': 'noreply@anthropic.com',
'timestamp': 1787690023,
'decoration': '',
'signature': 'N',
'subject': 'Add gheasy.repo: git repository operations as a literate notebook'}],
'summary': {'files': 8, 'additions': 8067, 'deletions': 2566, 'binary': 0},
'can_apply': False}}
Mutate with a way back
Guarded mutations record a safepoint before they run. The result includes an undo token. Pass that token to undo to restore the earlier branch, commit, and uncommitted work.
result = repo.checkout('feature')
print(result['summary'])
repo.undo(result['undo'])
# Apply the strategy selected after reviewing `divergence`.
repo.sync(preview['recommended'])
Review branches and commits
review compares branch work from the merge base. review_file loads one changed file on demand. commit_review and commit_file provide the same view for one commit.
review = repo.review('main', 'claude/ramabana-branching-review-uqj6v2')
review['summary'], review['commits']
file_review = repo.review_file('main', 'claude/ramabana-branching-review-uqj6v2', review['files'][0]['path'])
file_review['left'], file_review['right'], file_review['patch']
('# Release notes\n\n<!-- do not remove -->\n\n## 0.0.7\nghapi is async, so sync=True for now\n\n\n\n## 0.0.6\ngheasy new setup python requires to >=3.11\n\n\n\n## 0.0.5\npins python to 3.13, git workflows optional\n\n\n\n## 0.0.4\nnbdev pyproject to hatchling bugfix + cli addition\n\n\n\n## 0.0.3\nskills\n\n\n\n## 0.0.2\ngheasy makes git lfs, worfklows easy\n\n\n\n## 0.0.1\ninitial release\n\n\n',
"# Release notes\n\n<!-- do not remove -->\n\n## 0.0.8\n\nNew `gheasy.repo`: git repository operations, moved here from `ramabana.git`. One gateway that\nserialises every git process per repository, a safepoint before every mutation, and previews that\nrehearse a merge or a rebase without touching the worktree. `gheasy.core`'s own git calls now go\nthrough the same gateway.\n\n\n## 0.0.7\nghapi is async, so sync=True for now\n\n\n\n## 0.0.6\ngheasy new setup python requires to >=3.11\n\n\n\n## 0.0.5\npins python to 3.13, git workflows optional\n\n\n\n## 0.0.4\nnbdev pyproject to hatchling bugfix + cli addition\n\n\n\n## 0.0.3\nskills\n\n\n\n## 0.0.2\ngheasy makes git lfs, worfklows easy\n\n\n\n## 0.0.1\ninitial release\n\n\n",
"diff --git a/CHANGELOG.md b/CHANGELOG.md\nindex 54062f3..eb0aabf 100644\n--- a/CHANGELOG.md\n+++ b/CHANGELOG.md\n@@ -2,6 +2,14 @@\n \n <!-- do not remove -->\n \n+## 0.0.8\n+\n+New `gheasy.repo`: git repository operations, moved here from `ramabana.git`. One gateway that\n+serialises every git process per repository, a safepoint before every mutation, and previews that\n+rehearse a merge or a rebase without touching the worktree. `gheasy.core`'s own git calls now go\n+through the same gateway.\n+\n+\n ## 0.0.7\n ghapi is async, so sync=True for now\n \n")
Build a library pipeline
This workflow runs lint, then tests, then publishes a release to PyPI:
from gheasy.workflow import Workflow
wfb = Workflow("ci")
wfb.on.push(branches=["main"]).pull_request()
wfb.uv_lint_job()
wfb.uv_test_job(needs="lint")
wfb.uv_pypi_job(needs="test")
print(wfb.build().to_yaml())
name: ci
on:
push:
branches:
- main
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup uv
uses: astral-sh/setup-uv@v5
- name: Install dependencies
run: uv sync --frozen
- name: Lint
run: uv run ruff check . && uv run ruff format --check .
test:
needs: lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup uv
uses: astral-sh/setup-uv@v5
- name: Install dependencies
run: uv sync --frozen
- name: Test
run: uv run pytest
publish:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'release'
permissions:
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup uv
uses: astral-sh/setup-uv@v5
- name: Build
run: uv build
- name: Publish
uses: pypa/gh-action-pypi-publish@release/v1
Build an application pipeline
Use the workflow builder directly when a deployment needs custom steps. This example tests every push and deploys main to Fly.io:
wfb = Workflow("deploy")
wfb.on.push(branches=["main"])
wfb.uv_test_job()
wfb.job("deploy").needs("test").runs_on("ubuntu-latest")\
.checkout().end_step()\
.setup_uv().end_step()\
.step("Deploy to Fly.io").run("fly deploy --remote-only").end_job()
print(wfb.build().to_yaml())
name: deploy
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup uv
uses: astral-sh/setup-uv@v5
- name: Install dependencies
run: uv sync --frozen
- name: Test
run: uv run pytest
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup uv
uses: astral-sh/setup-uv@v5
- name: Deploy to Fly.io
run: fly deploy --remote-only
Configure LFS and secrets
Projects can configure Git LFS and copy local environment values to GitHub secrets or variables:
from gheasy.core import gh_lfs, gh_secrets_from_file, gh_push_env, GheasyConfig
# 1. Track binary/media files in git-lfs
gh_lfs(['*.mp3', '*.ogg', '*.wav', '*.png', '*.jpg', '*.webp', '*.xml', '*.db'])
# 2. Push .env values to GitHub — None-default keys become secrets, string-default become variables
cfg = GheasyConfig(app='vedicreader', env_schema={
# variables (have sane defaults)
'MODE': 'dev', 'PORT': '5001', 'DOMAIN': 'http://localhost:5001',
'WANT_GOOGLE': 'true', 'WANT_GIT': 'false',
'NEED_BACKUP': 'false', 'RC_TYPE': 's3', 'RC_PROVIDER': 'Cloudflare',
# secrets (no default — must be set)
'JWT_SCRT': None, 'RESEND_API_KEY': None,
'GOOGLE_CLI': None, 'GOOGLE_SCRT': None,
'CF_ACCESS_KEY_ID': None, 'CF_SCRT_ACCESS_KEY': None, 'CF_ENDPOINT': None,
})
cfg.save()
# reads local .env, routes each key: None-schema → gh secret set, string-schema → gh variable set
import os
gh_push_env(dict(os.environ))
Without a schema, gh_secrets_from_file sends every value in .env as a secret:
# Push all KEY=VALUE pairs from .env as GitHub secrets
gh_secrets_from_file('.env')
# With dry-run to preview what would be pushed
gh_secrets_from_file('.env', dry_run=True)
Build a cross-package pipeline
Workflow steps can call code from another package. This example uses dockeasy to generate a Dockerfile, then builds and publishes the image when its source files change:
wfb = Workflow("Build caddy-sqlite image")
wfb.on.push(branches=["main"], paths=["dockeasy/proxy.py", "nbs/01_proxy.ipynb"])
wfb.on.workflow_dispatch()
wfb.job("build-and-push").runs_on("ubuntu-latest")\
.permissions(contents="read", packages="write")\
.checkout().end_step()\
.setup_uv().end_step()\
.step("Install deps").run("uv sync").end_step()\
.step("Generate Dockerfile").run(
'uv run python -c "\n'
'from dockeasy.proxy import caddy_sqlite_dockerfile\n'
'caddy_sqlite_dockerfile().save(\'Dockerfile\')\n'
'"'
).end_step()\
.step("Log in to GHCR").uses("docker/login-action@v3")\
.with_(registry="ghcr.io",
username="${{ github.actor }}",
password="${{ secrets.GITHUB_TOKEN }}").end_step()\
.step("Build and push").uses("docker/build-push-action@v5")\
.with_(context=".", push=True,
tags="ghcr.io/vedicreader/caddy-sqlite:latest").end_job()
print(wfb.build().to_yaml())
name: Build caddy-sqlite image
on:
push:
branches:
- main
paths:
- dockeasy/proxy.py
- nbs/01_proxy.ipynb
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup uv
uses: astral-sh/setup-uv@v5
- name: Install deps
run: uv sync
- name: Generate Dockerfile
run: "uv run python -c \"\nfrom dockeasy.proxy import caddy_sqlite_dockerfile\ncaddy_sqlite_dockerfile().save('Dockerfile')\n\
\""
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/vedicreader/caddy-sqlite:latest
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gheasy-0.0.10.tar.gz.
File metadata
- Download URL: gheasy-0.0.10.tar.gz
- Upload date:
- Size: 59.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
657dc5fc40c5ea962ad2d6d4b07b1b7eace2cd383d1723ee4de954b09f36361f
|
|
| MD5 |
4f9bb325dcf4fe1a64d3776a808f1d6d
|
|
| BLAKE2b-256 |
e84da47031eda26dcda6a07b2260e1e9641c21363532e9e6f5741db1b2b10c69
|
File details
Details for the file gheasy-0.0.10-py3-none-any.whl.
File metadata
- Download URL: gheasy-0.0.10-py3-none-any.whl
- Upload date:
- Size: 61.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6bf88f60435f2f25ca48c856e2d4d4d5e74b6133b3a7fcfd6a5a5088a830cf30
|
|
| MD5 |
888264e6d5a7065f310399e9063527f2
|
|
| BLAKE2b-256 |
483666cf858718dad5ade90bb1df76d900f77aef25027b6c869c5c9fee490f49
|