dagger/dagger · error

count lines of added %s: %w

Error message

count lines of added %s: %w

What it means

computeChangesetPathsDelta wraps any failure from countGitLines when counting lines of a newly added file (withStats mode, no rename detection). countGitLines Lstats and reads the added file under afterDir to produce git-numstat-style added line counts, so this error means the added file could not be stat'ed or read at diff time.

Source

Thrown at core/changeset_delta.go:119

		if withStats {
			stats, err = compareDirectoriesNumStat(ctx, tmpBefore, tmpAfter)
			if err != nil {
				return nil, nil, fmt.Errorf("numstat delta files: %w", err)
			}
		}
	}

	if withStats {
		if stats == nil {
			stats = make(map[string]lineChanges)
		}
		if !detectRenames {
			// Added/removed files weren't staged for git; their counts are
			// just the file's own line count.
			for _, rel := range fc.Added {
				lines, ok, err := countGitLines(filepath.Join(afterDir, rel))
				if err != nil {
					return nil, nil, fmt.Errorf("count lines of added %s: %w", rel, err)
				}
				if ok {
					stats[rel] = lineChanges{Added: lines}
				}
			}
			for _, rel := range fc.Removed {
				lines, ok, err := countGitLines(filepath.Join(beforeDir, rel))
				if err != nil {
					return nil, nil, fmt.Errorf("count lines of removed %s: %w", rel, err)
				}
				if ok {
					stats[rel] = lineChanges{Removed: lines}
				}
			}
		}
	}

	renamedNew := make([]string, 0, len(fc.Renamed))

View on GitHub (pinned to 82ba2681db)

Solutions

  1. Re-run the diff against stable copies of both trees (or re-materialize snapshots) so files do not change mid-walk
  2. Check permissions/ownership on the added file under afterDir and ensure the process can read it
  3. Verify the added path exists on disk after the walk; if the tree mutates concurrently, serialize the diff
  4. If the file is intentionally transient, exclude it from the changeset input before diffing

Example fix

// before: diffing live mutating dirs directly
stats, err := computeChangesetPathsDelta(ctx, liveBeforeDir, liveAfterDir, true)
// after: snapshot/copy dirs first, then diff
beforeCopy, afterCopy := snapshotTrees(liveBeforeDir, liveAfterDir)
stats, err := computeChangesetPathsDelta(ctx, beforeCopy, afterCopy, true)
Defensive patterns

Strategy: validation

Validate before calling

for _, rel := range fc.Added {
    p := filepath.Join(afterDir, rel)
    if fi, err := os.Lstat(p); err != nil || !fi.Mode().IsRegular() {
        return fmt.Errorf("added file unreadable before stats: %s: %w", p, err)
    }
}

Type guard

func fileReadable(root, rel string) bool {
    fi, err := os.Lstat(filepath.Join(root, rel))
    return err == nil && (fi.Mode().IsRegular() || fi.Mode()&os.ModeSymlink != 0)
}

Try / catch

lines, ok, err := countGitLines(p)
if err != nil {
    var perr *fs.PathError
    if errors.As(err, &perr) && errors.Is(perr.Err, fs.ErrNotExist) {
        continue // file vanished mid-diff; skip or retry
    }
    return fmt.Errorf("count lines of added %s: %w", rel, err)
}

Prevention

When it happens

Trigger: Calling computeChangesetPathsDelta with withStats=true when the after tree contains an added file that disappears or becomes unreadable between the metadata walk and the counting pass (concurrent mutation), the path is a broken/dangling special file, or permission bits deny opening it.

Common situations: Diffing two mounted snapshot directories while a build concurrently rewrites them; a file deleted by a cleanup step mid-diff; a root-only-readable file in a container layer; overlay/whiteout oddities making a path vanish.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


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