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
- Check the wrapped errno in 'touch <path>: ...'; EPERM usually means ownership mismatch after copy
- Run the engine/merge as a user with permission to set timestamps on the work tree files, or normalize ownership
- Ensure the snapshot storage/overlay is writable (not EROFS)
- Exclude special file types from changesets if EINVAL on sockets/devices
- 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
- Run the engine in a mode where copied files keep the current uid (or run rootless-consistently)
- Ensure snapshot storage is on a writable filesystem
- Avoid changesets containing sockets/devices and other utimensat-incompatible file types
- Retry merges; the workspace is rebuilt from the base each time
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
- remove paths: %w
- copy changed paths: %w
- remove temporary merge git repository: %w
- failed to get mtime of bundle: %w
- failed to get mtime of updated bundle: %w
AI-assisted analysis of dagger/dagger@82ba2681db (2026-09-05).
Data as JSON: /api/errors/89500d9a4fecafd5.
Report an issue: GitHub.