gift

A push lands, and the script you named runs.

gift listens for GitHub events on a port of your own, and answers a push by running a script in a folder — build the site, pull the notes, restart the thing that just changed. Around that server sits a handful of git chores, run from the same command.

macOS · Linux·Node 18+·MIT

A push, and what it runs

A hook is a repository, the branches worth answering, a script, and the folder that script runs in. A delivery arrives and every hook watching that repository and that branch runs. Pick a push and watch it land:

Push
POST /hooks/githubsignature ok
push·gcc3/gcc3·refs/heads/main·3 commits
hooks
sitegcc3/gcc3main, master./build.sh~/sites/gcc3match
notesgcc3/gcc3-content*./pull.sh~/sites/gcc3/public/notes
giftlhypds/giftmaster./restart.sh~/gift
./build.sh ran in ~/sites/gcc3 · exit 0

The secret is checked before any of that: a delivery whose signature does not add up is refused. gift create can make the webhook on GitHub for you with gh, and then asks GitHub back whether it is really there — a hook in your file that GitHub never calls is worth being told about rather than assumed to work.

What gift create asks

Repository
The one whose pushes this hook answers, as owner/name.
Branches
Comma separated, or * for any; main, master unless you say otherwise. A push to a branch outside the list is answered with No match and runs nothing.
Name
A label, so gift list reads as something. Names may be reused — same-named hooks are deleted by their position in the list.
Script
What to run when a delivery matches.
Working directory
Where to run it. The server restarts itself once the hook is written, so there is nothing else to do.

The server

gift serve
Pulls the latest code, rebuilds, and starts listening.
gift restart
Puts the server back on the code already on disk — no pull, no rebuild, so it needs no network and returns straight away.
gift stop · gift status
Stops it; says whether it is running.
gift list · gift create · gift delete
Show the hooks, add one, remove one. The server restarts itself after either change.
gift log
The last ten lines, and then each line as the server writes it. gift log 20 for more, --no-follow to print them and exit.
gift config
Opens config.json in $EDITOR; --path prints where it is.
gift update
A git pull --ff-only in the folder gift is installed from — so it is the checkout's way of upgrading, and has nothing to pull in a release install.

The port also carries a page of its own: the hooks as they are configured, and the last day of deliveries newest first, with what each one's script printed — filling in while it is still printing it.

The other things it does

The same command runs a small set of git chores. gift help lists them, gift run opens a picker — a cursor to move, enter to run, a number key to run that row straight away — and gift <name> skips the picker. Enough of a name will do, and anything after it is passed on to the function itself.

repo-master

Every git repository under one folder, in a table that keeps itself up to date: which branch each is on, whether anything is uncommitted, how many lines that is, and when it last moved. Pick some rows and the menu is what may be done to them — open one in an editor or an agent, read a diff, fetch, pull, push, branch, merge, rebase, stash, or commit and push the lot. Nested checkouts and submodules get a row of their own.

runs asgift repo

weekly-prs

A week of pull requests, grouped by the day they were opened. Weeks run Monday to Sunday, and it asks how many weeks back unless you have already said.

runs asgift weekly

pull-repos

git pull --recurse-submodules --autostash in every repository below one folder, however deep it is. --dry-run says what it would pull without pulling it.

runs asgift pull

One file of settings

Every setting is in one file — config.json, beside the code. gift config opens it in your editor. gift's own settings are at the top level and each function's are under functions.<name>:

{
    "github_webhook_secret": "…",
    "port": 3999,
    "functions": {
        "repo-master": { "repo_root": "/Users/me/projects" },
        "weekly-prs": { "repos": "owner/repo1,owner/repo2", "author": "octocat" }
    }
}

It is written on first use with the settings worth looking at already in it, at their defaults, so opening it shows what there is to set rather than a blank page. Each one is declared in a config.schema.json next to the code that reads it — where its default, its description and the environment variable it reaches a script as all come from. A value already in the environment wins over the file, and a flag wins over both. The file is git-ignored and written 0600, because the webhook secret is in it.

Getting it

One line. It takes the latest release, unpacks it into ~/.gift, and runs the setup and install steps from there — so it asks for the public delivery URL as it goes, and leaves the gift command on your PATH. Node 18 or newer, curl and unzip, and nothing else to install first.

curl -fsSL https://raw.githubusercontent.com/lhypds/gift/master/get.sh | bash

GIFT_INSTALL_DIR unpacks it somewhere other than ~/.gift, and GIFT_INSTALL_VERSION=v0.0.1 pins a release rather than taking the newest.

Upgrading is the same line again: config.json, hooks.json and the logs are carried across, so the webhook secret and the hooks you configured survive the swap. A download that fails leaves the install that was working exactly where it was.

  1. Install it — the line above. It asks for the public URL that GitHub will deliver to, which is the one thing it cannot work out for itself.
  2. gift config — the port to listen on, and the webhook secret to check deliveries against.
  3. gift create — the repository, the branches, the script and where to run it. Answer yes and gh makes the webhook on GitHub for you; answer no and you add it yourself under Settings ▸ Webhooks, with the same secret.
  4. gift serve — and the next push runs it. gift log to watch it happen.

~/.gift/uninstall.sh takes the gift command back off again, and deleting ~/.gift removes the rest; from a checkout it is ./uninstall.sh.