Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

forcepush

forcepush is a project-local CLI for creating, setting up, checkpointing, archiving, restoring, and operating Git worktrees.

Its archive model is deliberately small: Git already has a shared object store, so a worktree's source changes are checkpointed there before the checkout is deleted. Ignored dependency and build directories disappear with the checkout. Restoring recreates the worktree, turns the checkpoint back into uncommitted changes when it is safe to do so, and runs setup again.

Install and build

Node.js 20 or newer and Git are required.

npm install --global forcepush

To build from source instead:

npm install
npm run build
npm link

The package installs forcepush, the compatibility command force-push, and the shorter fp command.

Quick start

From the main clone or any linked worktree:

forcepush init --yes
forcepush create billing
forcepush run billing -- npm install
forcepush session billing
forcepush archive billing
forcepush unarchive billing

Initialization creates:

$FORCE_PUSH_HOME/settings.yaml user-level defaults and shared setup rules
.agents/force-push.yaml       committed project configuration
.force-push/state.json        local managed-worktree registry
.force-push/archives/         local archive manifests and opt-in ignored data
.force-push/worktrees/        default checkout location

FORCE_PUSH_HOME defaults to $XDG_STATE_HOME/force-push or ~/.local/state/force-push. The local registry, archives, and default checkout directory are added to .gitignore. The .agents configuration is intentionally not ignored so a team can share its setup recipe.

Configuration

There are two configuration layers. User-level settings.yaml holds personal setup rules and session profiles across projects. Project-level .agents/force-push.yaml or TOML is committed with the repository. Global setup, cleanup, and ignored-path rules run first; project rules are appended. Project session profiles override global profiles with the same name.

The generated project configuration looks like:

version: 1
worktrees:
  directory: .force-push/worktrees
  branchPrefix: work/
setup:
  files:
    - from: .env
      to: .env
      mode: symlink
      optional: true
    - from: .npmrc
      to: .npmrc
      mode: copy
      optional: true
  scripts:
    - npm install
cleanup:
  scripts:
    - npm run stop-dev || true
archive:
  includeIgnored:
    - .local-data
session:
  command:
    - /bin/sh
    - -l
  profiles:
    review:
      - /bin/sh
      - -lc
      - npm test && exec $SHELL -l

Run forcepush init --toml to create .agents/force-push.toml instead.

Setup sources are resolved from the main clone. Destinations are constrained to the worktree. A rule may copy or symlink a file or directory. Setup scripts run in order after creation and restoration with these variables:

Relative setup sources in user settings resolve from the directory containing settings.yaml; relative sources in project configuration resolve from the main clone.

  • FORCE_PUSH_MAIN_ROOT
  • FORCE_PUSH_WORKTREE
  • FORCE_PUSH_WORKTREE_NAME for run and session

Copy/symlink destinations and generated setup output should normally be ignored by Git. Otherwise they are real worktree changes and will be included in a checkpoint.

archive.includeIgnored is an explicit list of relative files or directories that are precious despite being ignored, such as a local development database. These paths are copied into the archive directory and restored after setup. It is not a glob list. Leave it empty for metadata-only archives.

Commands

Command Purpose
init [--toml] Create project configuration and local storage
settings [--show] Print/create the user-level settings path or contents
create NAME Create a branch/worktree, register it, and run setup
adopt PATH Register an existing worktree
list [--json] List managed, archived, main, and unmanaged worktrees for this project
list --all [--json] List managed worktrees across registered projects
status [NAME] [--json] Show Git changes or archived checkpoint state
path [NAME] Print a managed worktree path
setup [NAME] Re-run configured setup
run NAME -- COMMAND... Run a non-interactive command in a worktree
session [NAME] Start the configured interactive command, or a login shell
archive [NAME] Checkpoint changes, verify reachability, and remove the checkout
unarchive NAME Recreate the checkout and restore its WIP
remove [NAME] Remove a clean worktree without archiving
lock / unlock Protect or unprotect a worktree with Git's native lock
prune [--dry-run] Prune stale Git worktree administrative entries
doctor [--json] Validate config, registry, checkouts, manifests, and refs
config [--show] Print the config path or contents
archives [--json] List active and restored archive history
restore-data ID Retry restoring opt-in ignored data to an active worktree
forget ID --force Permanently delete archive metadata and its safety ref

Most commands accept a managed name, UUID, or absolute path. When run from a managed worktree, commands with an optional worktree argument target the current one.

Creating worktrees

forcepush create api                         # branch work/api from HEAD
forcepush create api --branch feature/api    # custom branch
forcepush create review --start-point v2.0.0 # custom starting ref
forcepush create inspect --detach            # detached worktree
forcepush create api --no-setup               # skip setup once

If a requested branch already exists and is not checked out elsewhere, it is reused. Worktree names cannot contain path separators.

Sessions and commands

session starts session.command from the project config. With no configured command, it starts the user's login shell. Named commands under session.profiles can be selected with --profile.

forcepush session api
forcepush session api --profile review
forcepush run api -- npm test
forcepush run api -- git status --short

Exit codes from the child command are preserved.

How checkpoint archiving works

Archive is ordered so the checkout is never deleted before its source state is provably reachable:

  1. Run optional cleanup scripts.
  2. Save explicitly configured ignored paths.
  3. Read the current HEAD, branch, and Git status.
  4. If dirty, run git add -A and create a local checkpoint commit with Force-Push-Archive and Force-Push-Parent trailers.
  5. Pin the clean HEAD or checkpoint under refs/force-push/archives/<archive-id>.
  6. Verify both the ref and commit object.
  7. Write .force-push/archives/<archive-id>/manifest.json atomically.
  8. Remove the worktree and prune Git's administrative entry.
  9. Mark the registry entry archived.

The branch is never deleted. Checkpoints are local; pushing is not required for local durability.

Unarchive performs the inverse:

  1. Verify that the branch still points to the checkpoint, or contains it as an ancestor.
  2. Recreate the worktree at its prior path (or --path).
  3. If the branch still points exactly at the checkpoint, run git reset --mixed <original-head>. Files stay on disk and become modified/untracked again.
  4. Mark the worktree active, run setup, and restore configured ignored paths.

If the branch advanced and still contains the checkpoint, restoration succeeds but keeps the checkpoint committed. If the branch diverged and no longer contains it, restoration refuses to proceed. The manifest and safety ref remain intact, and doctor verifies them.

For a clean archive, branch movement is also explicit: restoration refuses to substitute a different tree unless --latest is passed. If setup fails after source restoration, the checkout stays active, configured ignored data is still restored, and forcepush setup NAME can retry setup without repeating the archive restore. If copying opt-in ignored data itself fails, the source checkout still becomes active and the archive remains retained; fix the destination and use forcepush restore-data ARCHIVE_ID.

Staging caveat

The checkpoint uses git add -A, so the distinction between staged and unstaged changes is flattened. File contents and untracked source files are restored, but all restored WIP is unstaged. Ignored files are discarded unless explicitly listed in archive.includeIgnored.

Manual recovery

Every archived manifest records snapshotSha, originalHead, and archiveRef. If normal restoration is blocked, inspect them with:

forcepush status NAME --json
git show refs/force-push/archives/ARCHIVE_ID

Do not run forget --force until the work is merged, pushed, or otherwise preserved. Use forcepush archives to obtain the archive ID. Forgetting an actively archived worktree also removes its registry entry; forgetting retained history for an active worktree leaves the worktree registered. The command removes the safety ref and may make a detached or unmerged checkpoint unreachable.

Development

npm run check
npm test
npm pack --dry-run

The integration suite creates real temporary Git repositories and tests only through the compiled CLI and Git's public interface.

About

Checkpoint, archive, restore, and manage Git worktrees without keeping heavyweight setup files around.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages