4 Choosing Commits
dxtr edited this page 2026-10-16 22:12:27 +02:00

Choosing Commits

A project requires other projects, which require others in turn, each at a commit. stift chooses one commit of each project for the build, as Go chooses versions, by minimal version selection, with git's history in place of version numbers.

The rule

  1. Start from the main project, the one being built. Read its stiftfile, and the stiftfile of each project it requires, at the commit required, from stift's cache. A project without a stiftfile requires nothing.
  2. Follow every requirement met, at the commit it names, until no new pair of project and commit appears. The requirements of every commit met count, not only those of the commits finally chosen, so the result does not depend on the order of reading.
  3. For each project, choose the commit required of it that contains all the others: the newest, in the project's own history.
  4. The build list is the main project, and every project its requirements reach through the requirements of the commits chosen.

A requirement is met by its commit or any commit that contains it. No commit newer than one required comes in, and two machines with the same stiftfiles choose the same commits.

An example: cl-harfbuzz requires cl-ffi at A, and cl-ft2 requires it at B, a later commit of the same branch. Pyssel requires both libraries, and gets cl-ffi at B, which contains A.

Test requirements

:test t marks a requirement that only the project's own tests need, such as Parachute. It counts in the main project, and is ignored when the project is someone else's dependency: nobody who uses cl-harfbuzz needs Parachute for it.

When requirements diverge

When no commit required of a project contains all the others, they lie on either side of a fork, or of a branch a force-push rewrote. stift stops, and names each requirement and who made it:

The requirements on fakehacker.cc/dxtr/cl-ffi have diverged:
  fakehacker.cc/dxtr/cl-harfbuzz at 1a2b3c4d5e6f requires 4f0c1a2b3c4d (branch "faster-calls")
  fakehacker.cc/dxtr/cl-ft2 at 9d8e7f6a5b4c requires 7a6b5c4d3e2f
Neither commit contains the other.  Require a commit that contains both, such
as a merge, or choose one in the :replace of the main project's stiftfile, or
of the workspace's stiftwork:
  ("https://fakehacker.cc/dxtr/cl-ffi.git" :commit "...")

The main project settles it, by requiring a commit that contains them all, such as a merge of both sides, or by choosing one with a replacement.

The stiftfile holds the build list

The main project's stiftfile lists its whole build list, so that it alone says what is built, a moved pin shows in its diff, and setting up a build needs no walk through other stiftfiles. stift tidy keeps it so:

  • each project of the build list that the stiftfile does not require itself gets an :indirect t entry, at the commit chosen, with the ref that came with that commit;
  • an indirect entry that only test requirements bring in is marked :test t too, so that projects using this one skip it with them;
  • an indirect entry that nothing needs any more goes;
  • a requirement whose project needs a later commit moves to it. A requirement that follows a branch keeps the branch when the branch contains the new commit; otherwise the requirement takes the ref of the requirement that chose the commit.

The entries without :indirect are the user's: only get and remove add and remove them. get, remove and update tidy the stiftfile after their own change, and write nothing when tidying fails.

An indirect entry counts as a requirement of the main project, as in Go, so update may move it past what the projects that need it require. stift remove on an indirect entry sends it back to the commit the projects that need it require.

The check before a build

Outside a workspace, every command that sets up a build, list, download, setup, exec and test, first checks that the stiftfile holds the build list its requirements make. It reads the stiftfile of each commit the stiftfile lists, and stops when one of them requires a project the stiftfile does not list, or a commit the stiftfile's commit for that project does not contain:

/home/me/app/stiftfile does not hold the build list its requirements make:
  fakehacker.cc/dxtr/cl-ft2 at 9d8e7f6a5b4c requires fakehacker.cc/dxtr/cl-ffi at 7a6b5c4d3e2f, which the stiftfile does not list
Run stift tidy to bring it up to date.

It happens after an edit by hand, or a merge of two versions of the file. stift never tidies unasked. In a workspace, stift chooses from every requirement met instead, as tidy does, so the members' stiftfiles need not be tidy (see Workspaces).

Replacements

An entry in the :replace of the main project's stiftfile chooses what the build uses for a project, whatever its requirements say:

Entry What the build uses
(URL :commit ID) That commit of the project's repository
(URL :with URL2 :commit ID) That commit of another repository, such as a fork
(URL :local DIR) The files in a directory, relative to the main project's

A replaced project's stiftfile is read from its replacement, and its requirements count as any project's. A replaced project gets no indirect entry, and an entry it has stays as it is. :branch or :tag beside :commit is the ref update follows, in the repository the files come from.

Replacements count only in main projects: a library cannot rearrange the builds of the projects that use it.

Forks under the wrong name

A project's stiftfile names it with :project. When that names another project than the one required, the repository is a fork or a mirror, and stift stops. Require the original, and use the fork in its place with the replacement (ORIGINAL :with FORK :commit ID); or take the fork as it is, under the name it was required by, with (FORK :commit ID). A replaced project's name is not checked.

Two projects, one system

A project provides the systems whose .asd files are in its tree, but for those in hidden directories, whose names start with a dot, and, in a project with a stiftfile, those in vendor/, where stift vendor puts its copies of other projects (see Vendoring). When two projects of the build list provide one system, ASDF would load one of them without a word, so stift stops and names both, with who requires each. If they are copies of one library, keep one with a replacement. If one carries a copy of the other in its tree, leave the copy out with :exclude on the requirement of the project that carries it:

(:project "example.org/me/app"
 :require (("https://example.org/someone/foo.git" :branch "main"
            :commit "..." :exclude ("vendor/"))))

:exclude names directories relative to the project's root, and counts only in the main project. Two .asd files of one name within one project are left to ASDF, which takes the shallower.

Why and graph

stift why prints the chains of requirements that bring a project into the build list, the shortest through each of the stiftfile's own requirements that leads to it. It takes the project's identity, URL or name, or the name of a system it provides:

$ stift why cl-ffi
fakehacker.cc/dxtr/pyssel
  requires fakehacker.cc/dxtr/cl-gfx at 5e03cf5ae3ff
  which requires fakehacker.cc/dxtr/cl-ffi at b7248691986f

stift graph prints every requirement met, as tidy reads them, a line each: the project and commit that requires, then the project and commit required.

$ stift graph
fakehacker.cc/dxtr/pyssel fakehacker.cc/dxtr/cl-gfx@5e03cf5ae3ff
fakehacker.cc/dxtr/cl-gfx@5e03cf5ae3ff fakehacker.cc/dxtr/cl-ffi@b7248691986f