Metadata-Version: 2.5
Name: gheasy
Version: 0.0.9
Summary: github made easy
Project-URL: Repository, https://github.com/vedicreader/gheasy
Project-URL: Documentation, https://vedicreader.github.io/gheasy/
Author-email: Karthik <karthik.rajgopal@hotmail.com>
License: Apache-2.0
License-File: LICENSE
Keywords: nbdev
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.11
Requires-Dist: cyclopts>=0.23
Requires-Dist: ghapi>=1.0.13
Requires-Dist: pyyaml>=6.0
Requires-Dist: ruamel-yaml>=0.18
Requires-Dist: tomlkit>=0.14.0
Description-Content-Type: text/markdown

# gheasy


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

## Install

``` bash
pip install gheasy
```

## Generate a CI workflow

[`uv_ci`](https://vedicreader.github.io/gheasy/workflow.html#uv_ci) creates a complete Python CI workflow:

``` python
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`](https://vedicreader.github.io/gheasy/repo.html#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.

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

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

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

``` python
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:

``` python
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`](https://vedicreader.github.io/gheasy/core.html#main) to Fly.io:

``` python
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:

``` python
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`](https://vedicreader.github.io/gheasy/core.html#gh_secrets_from_file) sends every value in `.env` as a secret:

``` python
# 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:

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