dagger/dagger · error

path modified in one changeset and removed in the other

Error message

path modified in one changeset and removed in the other

What it means

ErrModifiedRemoved is a sentinel error returned when one changeset modifies a path while the other removes it. This modify/delete conflict is unreconcilable automatically, so the merge fails with a conflict entry; the path is also called out specially in the aggregate message (Conflicts.Error groups these paths).

Source

Thrown at core/changeset.go:898

type ChangeType int

const (
	ChangeTypeAdded ChangeType = iota
	ChangeTypeModified
	ChangeTypeRemoved
)

type Conflict struct {
	Path  string
	Self  ChangeType
	Other ChangeType
	Err   error
}

var (
	ErrAddedTwice      = errors.New("path added in both changesets")
	ErrModifiedTwice   = errors.New("path modified in both changesets")
	ErrModifiedRemoved = errors.New("path modified in one changeset and removed in the other")
)

type Conflicts []Conflict

func (conflicts Conflicts) Error() (err error) {
	for _, c := range conflicts {
		err = errors.Join(err, fmt.Errorf("conflict between changesets at path %q: %w", c.Path, c.Err))
	}
	return err
}

func (conflicts Conflicts) IsEmpty() bool {
	return len(conflicts) == 0
}

func (conflicts Conflicts) ModifyDeletePaths() []string {
	var paths []string
	for _, c := range conflicts {

View on GitHub (pinned to 82ba2681db)

Solutions

  1. Decide whether the file should exist: drop the modification from the surviving changeset, or re-add the file in the deleting one.
  2. Rebase one changeset onto the other so the delete supersedes (or incorporates) the modification.
  3. Use errors.Is(err, ErrModifiedRemoved) to find affected paths via the Conflicts list and resolve each explicitly.
  4. Rebuild a single changeset reflecting the intended final state of the path.

Example fix

// before
merged, err := cs1.Merge(cs2) // fails: modified in one, removed in other
// after
if conflicts, ok := err.(changeset.Conflicts); ok {
    for _, c := range conflicts {
        if errors.Is(c.Err, changeset.ErrModifiedRemoved) {
            // choose: keep the delete
            cs1 = cs1.WithoutPath(c.Path)
        }
    }
    merged, err = cs1.Merge(cs2)
}
Defensive patterns

Strategy: type-guard

Validate before calling

// detect modify/delete pairs before merge
removed, modified := map[string]bool{}, map[string]bool{}
for _, c := range cs1.Changes() { (map[core.ChangeType]func(){...}) } // collect Removed into removed, Modified into modified
// conflict if removed[p] && modified[p] for any p

Type guard

func isModifiedRemoved(err error) bool { return errors.Is(err, core.ErrModifiedRemoved) }

Try / catch

if err := cs1.Merge(cs2); err != nil {
    if errors.Is(err, core.ErrModifiedRemoved) {
        // decide keep-vs-delete for the reported paths and rebuild one changeset
    }
    return err
}

Prevention

When it happens

Trigger: Merging changesets where one has ChangeTypeModified and the other ChangeTypeRemoved for the same path (core/changeset.go:965); detected in Conflicts.Error's special-case formatting at core/changeset.go:917.

Common situations: One session edited a file while another (or the base snapshot) deleted it; stale changesets replayed after files were removed; parallel refactors where one branch deletes a file another updates.

Related errors


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