dagger/dagger · error

touch %s: %w

Error message

touch %s: %w

What it means

touchAppliedPaths bumps mtimes of every added/modified file via unix.UtimesNanoAt so git's index stat check notices same-size edits; a failed utimensat (other than ENOENT, which is tolerated) is wrapped as 'touch <path>: <cause>'. Without this, git add with core.checkStat=minimal could silently skip re-hashing an edited file and drop the change from the merge commit.

Source

Thrown at core/changeset.go:1579

// touchAppliedPaths bumps the mtime of every path the changeset wrote so git
// can see the change. Snapshot contents carry normalized timestamps and the
// copier preserves them, so a same-size edit can leave a file's mtime and
// size both identical to the index entry; with core.checkStat=minimal (see
// gitEphemeralConfig) git add would then skip re-hashing it and silently
// drop the change from the branch commit.
func (ws *gitMergeWorkspace) touchAppliedPaths(paths *ChangesetPaths) error {
	for _, p := range slices.Concat(paths.Added, paths.Modified) {
		if strings.HasSuffix(p, "/") {
			// Directories are untracked by git; only file stat data matters.
			continue
		}
		full, err := RootPathWithoutFinalSymlink(ws.root, path.Join(ws.dir, p))
		if err != nil {
			return err
		}
		err = unix.UtimesNanoAt(unix.AT_FDCWD, full, nil, unix.AT_SYMLINK_NOFOLLOW)
		if err != nil && !errors.Is(err, unix.ENOENT) {
			return fmt.Errorf("touch %s: %w", p, err)
		}
	}
	return nil
}

// verifyMergedPaths confirms that every file-level path the given changesets
// declared as added or modified exists in the merged worktree. A conflict-free
// merge has no legitimate way to drop one; a missing path means the temporary
// repository lost applied content (a lost path here once meant a stale
// .git/HEAD from a changeset diff had redirected the ours commit onto the
// wrong branch, letting the merge silently resolve to one side).
func (ws *gitMergeWorkspace) verifyMergedPaths(contents ...*changesetContent) error {
	var missing []string
	for _, content := range contents {
		if content == nil {
			continue
		}
		paths := content.paths.withoutGitMeta()

View on GitHub (pinned to 82ba2681db)

Solutions

  1. Check the wrapped errno in 'touch <path>: ...'; EPERM usually means ownership mismatch after copy
  2. Run the engine/merge as a user with permission to set timestamps on the work tree files, or normalize ownership
  3. Ensure the snapshot storage/overlay is writable (not EROFS)
  4. Exclude special file types from changesets if EINVAL on sockets/devices
  5. Retry the merge; the workspace is rebuilt each run

Example fix

// before
err = unix.UtimesNanoAt(unix.AT_FDCWD, full, nil, unix.AT_SYMLINK_NOFOLLOW)
if err != nil && !errors.Is(err, unix.ENOENT) {
	return fmt.Errorf("touch %s: %w", p, err)
}
// after (caller-side workaround: chmod/chown the work tree so utimensat is permitted)
if err := os.Chown(workTreeRoot, currentUID, currentGID); err != nil {
	return fmt.Errorf("normalize work tree ownership: %w", err)
}
// then retry the merge operation
Defensive patterns

Strategy: try-catch

Validate before calling

// verify the merge work tree location is writable and ownership is consistent
info, err := os.Stat(workTreeMountPoint)
if err != nil || !isWritableByCurrentUser(info) {
	return fmt.Errorf("merge work tree not writable/owned: %w", err)
}

Type guard

func isTouchError(err error) (string, bool) {
	if err == nil {
		return "", false
	}
	path, ok := strings.CutPrefix(err.Error(), "touch ")
	return strings.TrimSpace(path), ok
}

Try / catch

err := dag.Directory().Merge(...)
if err != nil {
	if path, ok := isTouchError(err); ok && errors.Is(err, os.ErrPermission) {
		// normalize ownership/permissions of engine storage, then retry
		return fixOwnershipAndRetry(ctx, path)
	}
	return err
}

Prevention

When it happens

Trigger: unix.UtimesNanoAt fails on a path the changeset wrote — typically EACCES/EPERM (no ownership of the file after copy), EROFS on the mount, EINVAL/EBADF on unusual file types, or ENOSPC-type metadata failures. ENOENT is explicitly ignored, so this error means the file exists but its timestamp cannot be set.

Common situations: Copied files owned by a different uid in the merge work tree; rootless/userns setups restricting utimensat on files the process does not own; read-only upperdir; exotic file types (sockets/devices) in the diff that reject utimensat.

Related errors


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