--- /dev/null
+SHELL = /bin/sh
+
+# © 2023–2024 Lady [@ Lady’s Computer].
+#
+# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
+# If a copy of the MPL was not distributed with this file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
+
+BUILDTARGET := .grass
+DESTDIR := .
+
+CLIENTCHARSET := utf-8-mac
+SERVER := computer
+SERVERCHARSET := utf-8
+SERVERPATH := $(notdir $(abspath .))
+
+AWK := awk
+ECHO := echo
+SED := sed
+TEST := test
+
+GIT := git
+GITFORCE :=
+GITOPTS :=
+
+# This Makefile requires rsync 3 or newer.
+RSYNC := rsync
+ifneq ($(wildcard .rsync-filter),)
+RSYNCFILTER := .rsync-filter
+endif
+RSYNCOPTS := --checksum --compress --del --links --omit-dir-times --prune-empty-dirs --recursive --times --verbose
+
+override comma := ,
+
+ifneq ($(wildcard $(BUILDTARGET)),)
+override buildtime := $(shell stat -f '%m' $(BUILDTARGET))
+endif
+
+override committime := $(shell $(GIT) $(GITOPTS) log -1 --format='%ct' | $(AWK) '{print $$NF}' || true)
+ifneq ($(committime),)
+override gitstatus := $(shell $(GIT) $(GITOPTS) status --porcelain)
+endif
+
+ensure-build:
+ifeq ($(committime),)
+ @$(ECHO) 'Error: Unable to get commit time of most recent commit!' >&2
+ @false
+endif
+ifeq ($(buildtime),)
+ @$(ECHO) 'Error: The website has not been built yet!' >&2
+ @$(ECHO) 'Run `make´ before syncing.' >&2
+ @false
+endif
+ @if $(TEST) '$(committime)' -gt '$(buildtime)'; then $(ECHO) 'Error: A commit was made after the last build!' >&2; $(ECHO) 'Run `make´ before syncing.' >&2; false; fi
+
+ensure-branch-up-to-date:
+ifeq ($(committime),)
+ @$(ECHO) 'Error: Unable to get commit time of most recent commit!' >&2
+ @false
+endif
+ $(GIT)$(if $(GITOPTS), $(GITOPTS),) fetch
+ @if ! $(GIT) $(GITOPTS) merge-base --is-ancestor @{u} HEAD; then $(ECHO) 'Error: This branch is currently out‐of‐date!' >&2; $(ECHO) 'Pull in changes with `$(GIT)$(if $(GITOPTS), $(GITOPTS),) pull´ before syncing.' >&2; false; fi
+
+ensure-clean:
+ifneq ($(gitstatus),)
+ @$(ECHO) 'Error: There are uncommitted changes!' >&2
+ @$(ECHO) 'Commit changes and run `make´ before syncing.' >&2
+ @false
+endif
+
+dry-sync: ensure-clean$(if $(GITFORCE),, ensure-branch-up-to-date) ensure-build
+ cd $(DESTDIR) && $(RSYNC) --dry-run$(if $(RSYNCFILTER), --filter='. $(abspath $(RSYNCFILTER))',)$(if $(and $(CLIENTCHARSET),$(SERVERCHARSET),$(filter-out $(CLIENTCHARSET),$(SERVERCHARSET))), --iconv='$(CLIENTCHARSET)$(comma)$(SERVERCHARSET)',) $(RSYNCOPTS) . $(SERVER):$(SERVERPATH)
+
+sync: ensure-clean$(if $(GITFORCE),, ensure-branch-up-to-date) ensure-build
+ $(GIT)$(if $(GITOPTS), $(GITOPTS),) push$(if $(GITFORCE), --force,)
+ cd $(DESTDIR) && $(RSYNC)$(if $(RSYNCFILTER), --filter='. $(abspath $(RSYNCFILTER))',)$(if $(and $(CLIENTCHARSET),$(SERVERCHARSET),$(filter-out $(CLIENTCHARSET),$(SERVERCHARSET))), --iconv='$(CLIENTCHARSET)$(comma)$(SERVERCHARSET)',) $(RSYNCOPTS) . $(SERVER):$(SERVERPATH)
+
+.PHONY: dry-sync ensure-branch-up-to-date ensure-build ensure-clean sync;
--- /dev/null
+# 👥📤 Yseme
+
+**A make·file for syncing.**
+
+👥📤 Yseme is an implementation of the sync process described in my
+ blogpost [<cite>C·I pipelines have you down? Why not ‹ touch
+ .grass ›?!</cite>][touch_grass].
+It is intended to be included in projects as a Git submodule, and
+ recursively called from within another make·file.
+
+**Note:**
+👥📤 Yseme requires functionality present in G·N·U Make 3.81 (or
+ later) and will not work in previous versions, or other
+ implementations of Make.
+Compatibility with later versions of G·N·U Make is assumed, but not
+ tested.
+
+## Nomenclature
+
+<i>Yseme</i> combines two archaic English terms: <i>y‐</i> (indicating
+ togetherness or wholeness) and <i>seme</i> (< Old Norse <i>sǿmr</i>,
+ meaning “seemly”, “appropriate”, or “beautiful”).
+
+## Basic Usage
+
+`make dry-sync` will ensure that the current repository is clean,
+ built, and up‐to‐date, and then perform a dry run (`--dry-run`) of
+ R·Sync.
+`make sync` will perform the same checks, then push any new commits and
+ perform an actual run of R·Sync.
+
+👥📤 Yseme is intended to be called recursively from with·in another
+ make·file.
+The following is a sample make·file which makes use of 👥📤 Yseme :—
+
+```make
+SHELL = /bin/sh
+
+# © 2024 Lady [@ Lady’s Computer].
+#
+# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
+# If a copy of the MPL was not distributed with this file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
+
+# Assume 👥📤 Yseme is a git submodule located at <.yseme> in the
+# current directory.
+YSEME := .yseme
+YSEMEVARS := DESTDIR='public'
+
+# Build script (substitute with your own).
+build:
+ if [ ! -d public ]; then mkdir public; fi
+ echo 'Hello, world!' > public/index
+ touch .grass
+
+# Set up the 👥📤 Yseme submodule.
+$(YSEME)/GNUmakefile:
+ git submodule update --init $(YSEME)
+
+# Reload this make·file if the 👥📤 Yseme make·file has changed.
+# (Replace `GNUmakefile´ with whatever your make·file is called.)
+#
+# This is required to ensure the correct value of the `YSEME_TARGETS´ variable.
+#
+# See <https://www.gnu.org/software/make/manual/html_node/Remaking-Makefiles.html>.
+GNUmakefile: $(YSEME)/GNUmakefile
+ touch GNUmakefile
+
+# Ensure the 👥📤 Yseme make·file exists before running the following commands.
+ifneq ($(wildcard $(YSEME)/GNUmakefile),)
+# Programmatically get the list of phony targets defined by 👥📤 Yseme.
+# You can also list these manually (e·g `sync´, `dry-sync´).
+YSEME_TARGETS := $(shell sed '/^\.PHONY[ :]/!d;/^\.PHONY[ :]/s/ *;.*//;/^\.PHONY[ :]/s/\.PHONY.*: *//' < $(YSEME)/GNUmakefile)
+
+# For each target defined by 👥📤 Yseme, forward it appropriately.
+$(YSEME_TARGETS):
+ $(MAKE) -f $(YSEME)/GNUmakefile $@ $(YSEMEVARS)
+endif
+```
+
+👥📤 Yseme was designed and tested with G·N·U Make 3.81.
+It likely works with newer versions of Make, but may break in older
+ versions.
+
+## Setup and Configuration
+
+👥📤 Yseme depends on the following programs to run.
+In every case, you may supply your own implementation by overriding the
+ corresponding (allcaps) variable (e·g, set `RSYNC` to supply your own
+ `rsync` implementation).
+
+- `awk`
+- `echo`
+- `git`
+- `rsync` (version 3.0 or later)
+- `sed`
+- `test`
+
+The following varibales provide general configuration :—
+
+- **`BUILDTARGET`:**
+ A file whose timestamp is guaranteed to be updated on every build
+ (default: `.grass`).
+ 👥📤 Yseme will compare the modified time of this file to the commit
+ time of the latest commit to determine if the build is out·of·date.
+
+- **`DESTDIR`:**
+ The directory which contains the result of the build.
+ `rsync` will be called from this directory.
+
+The following variables configure Git :—
+
+- **`GITFORCE`:**
+ If this variable has a value, the current branch does not need to be
+ up·to·date with its remote counterpart and a force‐push will be
+ used (default: empty).
+
+- **`GITOPTS`:**
+ Options to pass to Git (default: empty).
+
+The following variables configuer R·Sync :—
+
+- **`CLIENTCHARSET`:**
+ The character set of the local machine (default: `utf-8-mac`).
+ If both this and `SERVERCHARSET` are set and not equal, an appropriate
+ `--iconv` option will be added to the R·Sync call.
+
+- **`RSYNCFILTER`:**
+ The location (relative to the working directory) of an R·Sync filter
+ file (default: `.rsync-filter` if such a file exists; otherwise,
+ empty).
+
+- **`RSYNCOPTS`:**
+ Option to use when calling R·Sync.
+ The following flags are set by default :—
+ `--checksum`, `--compress`, `--del`, `--links`, `--omit-dir-times`,
+ `--prune-empty-dirs`, `--recursive`, `--times`, `--verbose`.
+ Additional flags may be set depending on the values of variables and
+ which target is being built (e·g `dry-sync` 🆚 `sync`).
+
+- **`SERVER`:**
+ The remote machine to sync to (default: `computer`).
+
+- **`SERVERCHARSET`:**
+ The character set of the remote machine (default: `utf-8`).
+ If both this and `CLIENTCHARSET` are set and not equal, an appropriate
+ `--iconv` option will be added to the R·Sync call.
+
+- **`SERVERPATH`:**
+ The path on the remote machine to sync to (default: the name of the
+ current directory).
+
+If you use a method of syncing your website other than `rsync`, you can
+ still use 👥📤 Yseme:
+Just define your own `sync` and `dry-sync` targets which depend on
+ 👥📤 Yseme’s `ensure-clean`, `ensure-branch-up-to-date`, and
+ `ensure-build`.
+
+[touch_grass]: <https://blog.ladys.computer/2023-05-06/touch_grass/> "C·I pipelines have you down? Why not ‹ touch .grass ›?!"