dagger/dagger · error

removal of %q was not applied to the merge worktree

Error message

removal of %q was not applied to the merge worktree

What it means

verifyBranchContentLanded checks that every path the changeset declares as removed is actually gone from the merge worktree (after resolving the root path without final symlinks). If os.Lstat finds the path still exists (anything other than ErrNotExist), Dagger fails rather than silently keeping a file that was supposed to be deleted. The empty staged diff triggered this deeper verification.

Source

Thrown at core/changeset.go:1958

			continue
		}
		if wt == nil || !bytes.Equal(wt, headBytes) {
			return fmt.Errorf("worktree content for %q differs from HEAD but git staged nothing (index stat cache failure)", p)
		}
	}

	for _, p := range paths.AllRemoved {
		if strings.HasSuffix(p, "/") {
			// A removed directory can coexist with paths the same changeset
			// re-adds beneath it; only file removals are checkable here.
			continue
		}
		full, err := RootPathWithoutFinalSymlink(ws.root, path.Join(ws.dir, p))
		if err != nil {
			return err
		}
		if _, err := os.Lstat(full); !errors.Is(err, os.ErrNotExist) {
			return fmt.Errorf("removal of %q was not applied to the merge worktree", p)
		}
	}

	if len(files) == 0 {
		return nil
	}
	if content.diff.Self() == nil {
		return fmt.Errorf("changeset declared file changes %v but materialized no diff content", files)
	}
	return ws.withDiffDir(ctx, content, func(diffDir string) error {
		for _, p := range files {
			same, err := ws.worktreeMatchesDiff(p, diffDir)
			if err != nil {
				return err
			}
			if !same {
				return fmt.Errorf("applied content for %q did not land in the merge worktree", p)
			}

View on GitHub (pinned to 82ba2681db)

Solutions

  1. Check whether another changeset in the same merge re-adds the same path; resolve the conflicting removal/re-add between changesets.
  2. Retry - if it was an apply race, the rebuilt workspace will not reproduce it.
  3. Verify the removal path matches the base tree exactly (no trailing-slash/casing/symlink mismatch) in the code that constructs the changeset (Directory.withoutDirectory etc.).
  4. If reproducible with a single unambiguous removal, report as a merge-integrity bug with the path and changeset construction code.

Example fix

// before: two changesets where d1 removes "out/" and d2 writes "out/result.txt"
// after: sequence them or remove only specific files:
//   d1 := base.withoutDirectory("out/tmp")
//   d2 := base.withNewFile("out/result.txt", "data")
Defensive patterns

Strategy: validation

Validate before calling

// Before merging, ensure no path is removed by one changeset and written by another:
func removalWriteConflict(cs []Changeset) bool {
    removed, written := map[string]bool{}, map[string]bool{}
    for _, c := range cs {
        for _, p := range c.Removed { removed[strings.TrimSuffix(p, "/")] = true }
        for _, p := range append(c.Added, c.Modified...) { written[strings.TrimSuffix(p, "/")] = true }
    }
    for p := range removed { if written[p] { return true } }
    return false
}

Prevention

When it happens

Trigger: A changeset removing path p (p in paths.AllRemoved) applied to the branch, `git add -A`/`diff --cached` staged nothing, and Lstat on root/dir/p still finds the entry - meaning the removal never landed or the path was recreated.

Common situations: The base tree had a file at p that the removal failed to delete due to apply-content races; another changeset in the same merge recreates p; path casing or symlink resolution differences causing Lstat to hit a different entry than the one removed.

Related errors


AI-assisted analysis of dagger/dagger@82ba2681db (2026-09-05). Data as JSON: /api/errors/d8ec2449290357fb. Report an issue: GitHub.