Skip to content

Instantly share code, notes, and snippets.

@intellectronica
Last active August 26, 2025 15:30
Show Gist options
  • Save intellectronica/bb491ea5e5de24728d25c2b8225a087c to your computer and use it in GitHub Desktop.
Save intellectronica/bb491ea5e5de24728d25c2b8225a087c to your computer and use it in GitHub Desktop.
Create a gist with multiple files (including binaries) in a directory

gistify.py

Create a gist with multiple files (including binaries) in a directory

By Eleanor Berger < @intellectronica > and Codex CLI / GPT-5.


usage: gistify.py [-h] [--public | --secret] [directory]

Create a GitHub Gist from a flat directory using gh and git.

positional arguments:
  directory   Directory to gistify (defaults to current directory)

options:
  -h, --help  show this help message and exit
  --public    Create a public gist
  --secret    Create a secret gist (default)
$ uv run gistify.py

$ uv run gistify.py /path/to/gistifiable-dir

$ uv run gistify.py --public
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.9"
# dependencies = []
# ///
import argparse
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Optional
from urllib.parse import urlparse
def which_or_die(cmd: str) -> str:
path = shutil.which(cmd)
if not path:
print(f"Error: required command '{cmd}' is not installed or not in PATH.", file=sys.stderr)
sys.exit(2)
return path
def run(cmd, *, cwd: Optional[Path] = None, input_text: Optional[str] = None) -> str:
try:
res = subprocess.run(
cmd,
cwd=str(cwd) if cwd else None,
input=input_text.encode() if input_text is not None else None,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
)
return res.stdout.decode().strip()
except subprocess.CalledProcessError as e:
out = e.stdout.decode(errors="ignore").strip()
err = e.stderr.decode(errors="ignore").strip()
msg = f"Command failed: {' '.join(cmd)}\nSTDOUT:\n{out}\nSTDERR:\n{err}"
raise RuntimeError(msg)
def ensure_flat_directory(src: Path) -> list[Path]:
entries = list(src.iterdir())
subdirs = [p for p in entries if p.is_dir() and p.name != ".git"]
if subdirs:
names = ", ".join(sorted(p.name for p in subdirs))
raise RuntimeError(f"Directory is not flat (subdirectories found: {names}). Gists cannot contain folders.")
files = [p for p in entries if p.is_file()]
if not files:
raise RuntimeError("No files to gistify in the directory.")
return files
def create_empty_gist(make_public: bool) -> str:
# GitHub requires at least one file when creating a gist.
# Create with a tiny placeholder, then we will overwrite/remove it after cloning.
cmd = ["gh", "gist", "create", "-f", ".placeholder"]
if make_public:
cmd.append("--public")
url = run(cmd, input_text="temporary placeholder; will be replaced")
if not url.startswith("http"):
raise RuntimeError(f"Unexpected output from gh gist create: {url}")
return url
def _gist_id_from_url(url: str) -> str:
"""Extract the gist ID from a gist URL string."""
parsed = urlparse(url)
# Path may be like "/<user>/<id>" or just "/<id>"
parts = [p for p in parsed.path.split("/") if p]
if not parts:
raise RuntimeError(f"Cannot parse gist id from URL: {url}")
gist_id = parts[-1]
# basic sanity: gist ids are hex-ish; don't strictly enforce
return gist_id
def clone_gist(url: str) -> Path:
# Prefer SSH to leverage user's SSH key without prompting for username/password
gist_id = _gist_id_from_url(url)
clone_url = f"[email protected]:{gist_id}.git"
tmpdir = Path(tempfile.mkdtemp(prefix="gistify-"))
run(["git", "clone", clone_url, str(tmpdir)])
return tmpdir
def copy_files(files: list[Path], dest: Path) -> None:
for f in files:
shutil.copy2(str(f), dest / f.name)
def commit_and_push(repo_dir: Path, src_dir: Path) -> None:
# Remove placeholder if present
placeholder = repo_dir / ".placeholder"
if placeholder.exists():
try:
placeholder.unlink()
except OSError:
pass
run(["git", "add", "-A"], cwd=repo_dir)
# Commit; handle no-op commit gracefully
try:
run(["git", "commit", "-m", f"Add files from {src_dir}"] , cwd=repo_dir)
except RuntimeError as e:
# If nothing to commit, ignore; otherwise re-raise
if "nothing to commit" not in str(e).lower():
raise
run(["git", "push"], cwd=repo_dir)
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Create a GitHub Gist from a flat directory using gh and git."
)
)
vis = parser.add_mutually_exclusive_group()
vis.add_argument("--public", action="store_true", help="Create a public gist")
vis.add_argument(
"--secret",
action="store_true",
help="Create a secret gist (default)",
)
parser.add_argument(
"directory",
nargs="?",
default=".",
help="Directory to gistify (defaults to current directory)",
)
return parser.parse_args(argv)
def main(argv: list[str]) -> int:
which_or_die("gh")
which_or_die("git")
args = parse_args(argv)
make_public = bool(args.public) and not bool(args.secret)
src = Path(args.directory).expanduser().resolve()
if not src.exists() or not src.is_dir():
print(f"Error: path is not a directory: {src}", file=sys.stderr)
return 2
try:
files = ensure_flat_directory(src)
gist_url = create_empty_gist(make_public)
repo_dir: Optional[Path] = None
try:
repo_dir = clone_gist(gist_url)
copy_files(files, repo_dir)
commit_and_push(repo_dir, src)
finally:
if repo_dir and repo_dir.exists():
shutil.rmtree(repo_dir, ignore_errors=True)
except RuntimeError as e:
print(str(e), file=sys.stderr)
return 1
print(gist_url)
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment