Skip to content

Instantly share code, notes, and snippets.

@AdamGagorik
Last active April 23, 2024 14:56
Show Gist options
  • Save AdamGagorik/fdcf21aa486a31e38ccdee8868ff9220 to your computer and use it in GitHub Desktop.
Save AdamGagorik/fdcf21aa486a31e38ccdee8868ff9220 to your computer and use it in GitHub Desktop.
Interactive gh gist callback
#!/usr/bin/env python3
"""
EXTRAS
path: Get path of local clone
link: Register a gist locally
sync: Sync all registered gists
debug: Debug script configuration
status: Get status of registered gists
select: Select a gist using fuzzy finder
attach: Attach a gist to the local registry
detach: Detach a gist from the local registry
modify: Modify gist selected via fuzzy finder
install: Bootstrap install this script locally
uninstall: Instructions for removing this script
NOTES
To attach a gist you do not personally own, use the full URL
gist attach https://gist.github.com/RichardBronosky/56d8f614fab2bacdd8b048fb58d0c0c7
"""
import io
import os
import shutil
from argparse import ArgumentParser
from pathlib import Path
from subprocess import PIPE, CompletedProcess, Popen, run
from typing import Any, Generator, Iterable
GIST_ID_PREFIX = "https://gist.github.com/"
THIS_GIST_HASH = "AdamGagorik/fdcf21aa486a31e38ccdee8868ff9220"
LOCAL_GIST_DIR = Path(os.environ.get("LOCAL_GIST_DIR", os.path.expanduser("~/.gists")))
LOCAL_GIST_BIN = Path(os.environ.get("LOCAL_GIST_BIN", LOCAL_GIST_DIR.joinpath("bin")))
GIT_EXECUTABLE = os.environ.get("GIT_EXECUTABLE", "git")
def args() -> dict[str, Any]:
parser = ArgumentParser(description="wrapper around gh gist", add_help=False)
parser.add_argument(
"action",
choices=[
"clone",
"create",
"delete",
"edit",
"list",
"rename",
"view",
# extras
"path",
"link",
"sync",
"debug",
"status",
"select",
"attach",
"detach",
"modify",
"install",
"uninstall",
],
nargs="?",
)
opts, remaining = parser.parse_known_args()
opts.extras = remaining
return opts.__dict__
def main(action: str | None, extras: Iterable[str] = ()) -> None:
match action:
case None:
default_help()
case "path":
gist = get_local_gist(*extras)
print(gist)
case "link":
assert not extras
link_all_gists()
case "sync":
assert not extras
sync_all_gists()
case "debug":
assert not extras
debug_all_gists()
case "status":
assert not extras
get_gist_status()
case "select":
g_id = get_g_id(*extras)
print(g_id)
case "attach":
g_id = get_g_id(*extras)
gist = clone_gist_local(g_id)
link_gist(gist)
case "detach":
gist = get_local_gist(*extras)
detach_local_gist(gist)
case "modify":
g_id = get_g_id(*extras)
if run_default_command("edit", g_id, *extras).returncode == 0:
sync_gist(gist=LOCAL_GIST_DIR.joinpath(g_id))
case "install":
run_bootstrap_install()
case "uninstall":
run_bootstrap_uninstall()
case _:
if run_default_command(action, *extras).returncode != 0:
raise SystemExit("Error running gh gist")
def agree(name: Any, c: str = "?", after: str = "") -> bool:
return input(
"{} {} [y/n] ".format(" {} ".format(name).center(80, c), after)
).lower() in ("y", "yes")
def title(name: Any, c: str = "-", after: str = "") -> None:
print("{} {}".format(" {} ".format(name).center(80, c), after))
def debug(*msg: str, **kwargs: Any) -> None:
print("".join(msg).format(**kwargs))
def default_help() -> None:
run(["gh", "gist", "--help"])
print(__doc__[1:-1])
def debug_all_gists() -> None:
title("REGISTERED")
run(["tree", LOCAL_GIST_DIR])
title("CONFIGURATION")
print("GIT_EXECUTABLE: {}".format(GIT_EXECUTABLE))
print("THIS_GIST_HASH: {}".format(THIS_GIST_HASH))
print("GIST_ID_PREFIX: {}".format(GIST_ID_PREFIX))
print("LOCAL_GIST_DIR: {}".format(LOCAL_GIST_DIR))
print("LOCAL_GIST_BIN: {}".format(LOCAL_GIST_BIN))
def get_local_path() -> str:
pass
def link_gist(gist: Path) -> None:
LOCAL_GIST_BIN.mkdir(exist_ok=True, parents=True)
for src in gist.iterdir():
if src.is_file():
if src.suffix.lower() not in (".md", ".txt"):
title(src.parent.name, "*", src.name)
dst = LOCAL_GIST_BIN.joinpath(src.name)
if dst.exists():
dst.unlink()
dst.symlink_to(src)
run(["chmod", "+x", dst])
def unlink_gist(gist: Path) -> None:
for src in gist.iterdir():
if src.is_file():
dst = LOCAL_GIST_BIN.joinpath(src.name)
if dst.exists():
title(src.parent, "*", src.name)
dst.unlink()
def walk_all_gist() -> Generator[Path, None, None]:
for root, dirs, files in os.walk(LOCAL_GIST_DIR):
if Path(root).name == ".git":
dirs[:] = []
else:
for basename in dirs:
path = Path(root).joinpath(basename)
if path.is_dir() and path.joinpath(".git").exists():
yield path
def link_all_gists() -> None:
title("LINK")
for gist in walk_all_gist():
link_gist(gist)
def sync_gist(gist: Path) -> None:
if not gist.exists():
title(gist, "*", "skipped, gist is not cloned locally")
return
title(gist.parent.name, "*")
p = run(
[GIT_EXECUTABLE, "-C", gist, "reset", "--hard"], capture_output=True, check=True
)
print(p.stdout.decode("utf-8", errors="ignore").rstrip())
p = run([GIT_EXECUTABLE, "-C", gist, "pull"], capture_output=True, check=True)
print(p.stdout.decode("utf-8", errors="ignore").rstrip())
def sync_all_gists() -> None:
title("SYNC")
for gist in walk_all_gist():
sync_gist(gist)
link_gist(gist)
def get_gist_status() -> None:
for gist in walk_all_gist():
title(gist, "*")
p = run([GIT_EXECUTABLE, "-C", gist, "status"], capture_output=True, check=True)
print(p.stdout.decode("utf-8", errors="ignore").rstrip())
def clone_gist_local(g_id: str) -> Path:
if g_id.startswith(GIST_ID_PREFIX):
prefix, _, suffix = g_id.rpartition(GIST_ID_PREFIX)
gist = LOCAL_GIST_DIR.joinpath(suffix)
g_id = suffix
else:
gist = LOCAL_GIST_DIR.joinpath(g_id)
if gist.exists():
sync_gist(gist)
return gist
else:
gist.mkdir(parents=True, exist_ok=True)
run([GIT_EXECUTABLE, "clone", f"{GIST_ID_PREFIX}/{g_id}", gist])
return gist
def detach_local_gist(gist: Path) -> None:
if not gist.exists():
title(gist, "*", "skipped, gist is not cloned locally")
return
if not gist.joinpath(".git").exists():
title(gist, "*", "skipped, gist is not a git repository")
return
if not gist.is_relative_to(LOCAL_GIST_DIR):
title(gist, "*", "skipped, gist is not inside LOCAL_GIST_DIR")
return
if agree(gist, "?", "Remove gist from local registry?"):
unlink_gist(gist)
shutil.rmtree(gist)
title(gist, "*", "deleted")
else:
title(gist, "*", "skipped")
def get_origin_url(gist: Path) -> str:
p = run(
[GIT_EXECUTABLE, "-C", f"{gist}", "remote", "get-url", "origin"],
capture_output=True,
check=True,
)
return p.stdout.decode("utf-8", errors="ignore").strip()
def set_origin_url(gist: Path, url: str) -> None:
run([GIT_EXECUTABLE, "-C", f"{gist}", "remote", "set-url", "origin", url])
def ssh_to_https_url(url: str) -> str:
return url.replace(r"https://", "git@").replace(".com/", ".com:")
def run_default_command(action: str, *extras: str) -> CompletedProcess:
return run(["gh", "gist", action, *extras])
def get_g_id(query: str = None) -> str:
if query is not None and query.startswith(GIST_ID_PREFIX):
return query
p1 = Popen(["gh", "gist", "list", "--limit", "256"], stdout=PIPE)
p2 = Popen(["column", "-s$\t", "-t"], stdin=p1.stdout, stdout=PIPE)
p3 = run(
[
"fzf",
"--preview=gh gist view {1}",
"--preview-label=[ gist ]",
"--preview-window=50%,border-double,bottom",
*(("--query", query) if query else ()),
],
stdin=p2.stdout,
stdout=PIPE,
)
if p3.returncode != 0:
raise SystemExit("No gist selected")
else:
return p3.stdout.decode("utf-8").split(" ")[0]
def get_local_gist(*query: str) -> Path:
stream = io.StringIO()
for gist in walk_all_gist():
members = [p for p in gist.iterdir() if p.is_file()]
stream.write(
f"{str(gist.relative_to(LOCAL_GIST_DIR)):32} {', '.join([p.name for p in members])}\n"
)
p3 = Popen(
[
"fzf",
f"--preview=cat {LOCAL_GIST_DIR.joinpath('{1}', '{2}')}",
"--preview-label=[ gist ]",
"--preview-window=50%,border-double,bottom",
*(("--query", " ".join(query)) if query else ()),
],
stdin=PIPE,
stdout=PIPE,
)
stdout, stderr = p3.communicate(input=stream.getvalue().encode("utf-8"))
if p3.returncode != 0:
raise SystemExit("No gist selected")
else:
g_id = stdout.decode("utf-8").split(" ")[0].strip()
return LOCAL_GIST_DIR.joinpath(g_id)
def get_gist_name(g_id: str) -> str:
return (
run(["gh", "api", f"gists/{g_id}", "--jq", ".files | keys[]"])
.stdout.decode("utf-8")
.strip()
)
def run_bootstrap_install() -> None:
gist = clone_gist_local(THIS_GIST_HASH)
link_gist(gist)
def run_bootstrap_uninstall() -> None:
print("Remove the following directories:")
print("LOCAL_GIST_DIR: {}".format(LOCAL_GIST_DIR))
print("LOCAL_GIST_BIN: {}".format(LOCAL_GIST_BIN))
print("Remove LOCAL_GIST_BIN from your PATH")
if __name__ == "__main__":
main(**args())
@AdamGagorik
Copy link
Author

AdamGagorik commented Oct 18, 2023

Requires

  • python3.12
  • fzf
  • git
  • gh

Setup

Bootstrap install it once with curl.

python <(curl -sL https://gist.githubusercontent.com/AdamGagorik/fdcf21aa486a31e38ccdee8868ff9220/raw/gist) install

Put the directory you ran it from on your path.

export PATH=~/.gists/bin:$PATH

Usage

Mostly it is a wrapper around gh gist command. Run gist --help to see the options.

Extras

# Create symlinks of all locally cloned gists into the bin directory.
gist link

# Run git pull on all locally cloned gists.
gist sync

# Debug configuration + show gist working tree.
gist debug

# Get status of all locally registered gists.
gist status

# Open fzf on all personal gists and select one.
gist select

# Open fzf on all personal gists and select one to download and link.
gist attach

# Open fzf on all downloaded gists and select one to unfetch.
gist detach

# Open fzf on all personal gists and select one to edit.
gist modify

# Install this script as a command (see above)
gist install

# Print how to uninstall this script
gist uninstall

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment