# Conflicts during replay

> Multi-commit cherry-pick and revert sequences, on-disk sequencer state, and continue/abort.

Pick and revert ranges use [`porcelain::replay_sequence`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/replay_sequence/index.html) instead of single-commit [`replay_commit`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/replay/fn.replay_commit.html). On conflict, grit writes Git-compatible `CHERRY_PICK_HEAD` or `REVERT_HEAD`, `MERGE_MSG`, `ORIG_HEAD`, and `.git/sequencer/` (todo, head, abort-safety, opts) so `git cherry-pick --continue` and `grit pick --continue` can interoperate.

[`ReplayOptions`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/replay_sequence/struct.ReplayOptions.html) controls mainline parent selection for merge commits, `-x` / `record_origin`, empty-commit policy (`Stop`, `Drop`, `Keep`), merge favor, and conflict marker style. [`start_replay_sequence`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/replay_sequence/fn.start_replay_sequence.html) returns [`SequenceOutcome`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/replay_sequence/enum.SequenceOutcome.html) — `Completed`, `Conflicts`, or `Empty`. Every commit created so far is a [`ReplayCreated`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/replay_sequence/struct.ReplayCreated.html) carrying its source, new oid, stored change id, and subject (also for steps committed before an earlier stop), and a stop reports the subject of the commit it stopped on, so callers can render results without re-reading commits. Resolve conflicts in the worktree, stage paths, then call [`continue_replay_sequence`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/replay_sequence/fn.continue_replay_sequence.html). [`skip_replay_step`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/replay_sequence/fn.skip_replay_step.html) and [`abort_replay_sequence`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/replay_sequence/fn.abort_replay_sequence.html) match Git's skip/abort semantics (abort refuses when `HEAD` moved past `sequencer/abort-safety`).

Conflict markers and unmerged index stages reuse the same tree-merge path as [`porcelain::merge`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/merge/index.html) (`apply_conflict_merge_worktree`). [`status`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/status/fn.status.html) surfaces cherry-pick/revert operation guidance via [`state::status_operation`](https://docs.rs/grit-lib/latest/grit_lib/state/fn.status_operation.html). On the returned model, [`StatusModel::conflict_groups`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/status/struct.StatusModel.html#method.conflict_groups) groups unmerged paths by [`ConflictKind`](https://docs.rs/grit-lib/latest/grit_lib/operation/enum.ConflictKind.html), and [`StatusModel::guidance`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/status/struct.StatusModel.html#method.guidance) picks the next step `grit status` suggests: the paused operation's runnable steps, the conflict and merge fallbacks, or everyday [`StatusAction`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/status/enum.StatusAction.html)s (discard, stage, commit, pull when strictly behind).

## One conflict model

Every API that reports conflicts uses [`operation::Conflict`](https://docs.rs/grit-lib/latest/grit_lib/operation/struct.Conflict.html) (a path and a [`ConflictKind`](https://docs.rs/grit-lib/latest/grit_lib/operation/enum.ConflictKind.html)): [`StatusModel::conflict_details`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/status/struct.StatusModel.html#structfield.conflict_details), merge and pull outcomes, [`SequenceOutcome::Conflicts`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/replay_sequence/enum.SequenceOutcome.html#variant.Conflicts), a stopped rebase's [`RebaseStop`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.RebaseStop.html), rebase predictions and [`HistoryError::Conflict`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.HistoryError.html#variant.Conflict), and [`StashApplyOutcome`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/stash/struct.StashApplyOutcome.html). The kind is read from which unmerged index stages a path has ([`conflict_kind_from_stagemask`](https://docs.rs/grit-lib/latest/grit_lib/operation/fn.conflict_kind_from_stagemask.html), the same classification as Git's porcelain v2 `u` lines), so an operation's conflicts equal what `status` reports right after it stops, and a UI needs one renderer. Only a tree-merge result ([`conflicts_from_tree_merge`](https://docs.rs/grit-lib/latest/grit_lib/operation/fn.conflicts_from_tree_merge.html)) can add [`ConflictKind::RenameRename`](https://docs.rs/grit-lib/latest/grit_lib/operation/enum.ConflictKind.html#variant.RenameRename) and the directory rename kinds ([`DirectoryRenameMoved`](https://docs.rs/grit-lib/latest/grit_lib/operation/enum.ConflictKind.html#variant.DirectoryRenameMoved), [`DirectoryRenameSplit`](https://docs.rs/grit-lib/latest/grit_lib/operation/enum.ConflictKind.html#variant.DirectoryRenameSplit), [`DirectoryRenameBlocked`](https://docs.rs/grit-lib/latest/grit_lib/operation/enum.ConflictKind.html#variant.DirectoryRenameBlocked)), which need information the index does not keep; [`conflicts_from_index`](https://docs.rs/grit-lib/latest/grit_lib/operation/fn.conflicts_from_index.html) classifies an index alone. [`ConflictKind::as_str`](https://docs.rs/grit-lib/latest/grit_lib/operation/enum.ConflictKind.html#method.as_str) gives the snake_case name `grit` prints in `--json`, and with the `serde-models` feature a `Conflict` serializes to the same `{"path", "kind"}` object.

```rust
use grit_lib::operation::ConflictKind;
use grit_lib::porcelain::status::{status, StatusOptions};
use grit_lib::progress::{NeverCancel, NullProgress};

let model = status(&repo, &StatusOptions::default(), &mut NullProgress, &NeverCancel)?;
for conflict in &model.conflict_details {
    if conflict.kind == ConflictKind::DeletedByUs {
        println!("{}: deleted here, changed on the incoming side", conflict.path);
    }
}
```

## Example

```rust
//! Cherry-pick / revert sequences that stop on conflicts (library API).
//!
//! Source for the library guide "Conflicts during replay" page.

use grit_lib::config::environment::RepositoryOptions;
use grit_lib::objects::ObjectId;
use grit_lib::porcelain::add::{stage, StageOptions};
use grit_lib::porcelain::replay::ReplayDirection;
use grit_lib::porcelain::replay_sequence::{
    continue_replay_sequence, start_replay_sequence, ReplayEmptyPolicy, ReplayOptions,
    ReplaySequenceRequest, SequenceOutcome,
};
use grit_lib::progress::NullProgress;
use grit_lib::repo::Repository;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

const IDENT: &str = "Library Guide <guide@grit-scm.com> 1735689600 +0000";

fn replay_identity() -> grit_lib::ident::Signature {
    grit_lib::ident::parse_signature(IDENT)
        .unwrap_or_else(|| panic!("guide IDENT must parse as a signature: {IDENT}"))
}

fn open_repo(root: &Path) -> Result<Repository, grit_lib::error::Error> {
    let git_dir = if root.join(".git").is_dir() {
        root.join(".git")
    } else {
        root.to_path_buf()
    };
    let work_tree = if root.join(".git").is_dir() {
        Some(root)
    } else {
        None
    };
    let mut opts = RepositoryOptions::empty();
    if let Some(wt) = work_tree {
        opts = opts.with_work_tree(wt);
    }
    Repository::open(&git_dir, &opts)
}

fn git(root: &Path, args: &[&str]) -> Result<(), grit_lib::error::Error> {
    let home = root.join(".git-home");
    fs::create_dir_all(&home).map_err(grit_lib::error::Error::Io)?;
    let out = Command::new("git")
        .current_dir(root)
        .args(args)
        .env("HOME", &home)
        .env("GIT_CONFIG_GLOBAL", "/dev/null")
        .env("GIT_CONFIG_SYSTEM", "/dev/null")
        .output()
        .map_err(grit_lib::error::Error::Io)?;
    if !out.status.success() {
        return Err(grit_lib::error::Error::Message(format!(
            "git {:?}: {}",
            args,
            String::from_utf8_lossy(&out.stderr)
        )));
    }
    Ok(())
}

fn oid_from_rev(root: &Path, rev: &str) -> Result<ObjectId, grit_lib::error::Error> {
    let home = root.join(".git-home");
    let out = Command::new("git")
        .current_dir(root)
        .args(["rev-parse", rev])
        .env("HOME", &home)
        .env("GIT_CONFIG_GLOBAL", "/dev/null")
        .env("GIT_CONFIG_SYSTEM", "/dev/null")
        .output()
        .map_err(grit_lib::error::Error::Io)?;
    if !out.status.success() {
        return Err(grit_lib::error::Error::Message("rev-parse failed".into()));
    }
    let hex = String::from_utf8_lossy(&out.stdout).trim().to_owned();
    ObjectId::from_hex(&hex).map_err(|_| grit_lib::error::Error::Message("bad oid".into()))
}

/// Build a tiny repo with one conflicting pick, run start → resolve → continue.
fn demo_conflict_sequence(root: &Path) -> Result<(), grit_lib::error::Error> {
    git(root, &["init", "-q", "-b", "main", "."])?;
    git(root, &["config", "user.email", "guide@grit-scm.com"])?;
    git(root, &["config", "user.name", "Library Guide"])?;
    fs::write(root.join("f"), "base\n").map_err(grit_lib::error::Error::Io)?;
    git(root, &["add", "f"])?;
    git(root, &["commit", "-qm", "base"])?;

    git(root, &["switch", "-c", "topic"])?;
    fs::write(root.join("f"), "topic\n").map_err(grit_lib::error::Error::Io)?;
    git(root, &["commit", "-qam", "topic"])?;
    let pick = oid_from_rev(root, "HEAD")?;

    git(root, &["switch", "main"])?;
    fs::write(root.join("f"), "main\n").map_err(grit_lib::error::Error::Io)?;
    git(root, &["commit", "-qam", "main"])?;

    let repo = open_repo(root)?;
    let req = ReplaySequenceRequest {
        commits: vec![pick],
        direction: ReplayDirection::Pick,
        committer: replay_identity(),
        options: ReplayOptions {
            empty: ReplayEmptyPolicy::Stop,
            ..ReplayOptions::default()
        },
    };

    match start_replay_sequence(&repo, &req)? {
        SequenceOutcome::Conflicts {
            conflicts,
            current,
            created_so_far,
            ..
        } => {
            println!(
                "stopped on {} conflict(s) at {}",
                conflicts.len(),
                current.to_hex()
            );
            println!("created_so_far={}", created_so_far.len());
        }
        other => {
            return Err(grit_lib::error::Error::Message(format!(
                "expected conflicts, got {other:?}"
            )));
        }
    }

    fs::write(root.join("f"), "merged\n").map_err(grit_lib::error::Error::Io)?;
    stage(
        &repo,
        &StageOptions::default(),
        &mut NullProgress,
        &grit_lib::progress::NeverCancel,
    )?;

    match continue_replay_sequence(&repo, &replay_identity())? {
        SequenceOutcome::Completed { created, dropped } => {
            println!(
                "sequence completed; created={} dropped={}",
                created.len(),
                dropped.len()
            );
            for record in created {
                println!(
                    "replay source={} new={} change_id={}",
                    record.source.to_hex(),
                    record.new_oid.to_hex(),
                    record
                        .change_id
                        .map(|c| c.to_string())
                        .unwrap_or_else(|| "-".into())
                );
            }
        }
        other => {
            return Err(grit_lib::error::Error::Message(format!(
                "expected completed after continue, got {other:?}"
            )));
        }
    }

    Ok(())
}

fn main() -> Result<(), grit_lib::error::Error> {
    if std::env::args().nth(1).is_none() {
        let temp = tempfile::tempdir().map_err(grit_lib::error::Error::Io)?;
        demo_conflict_sequence(temp.path())?;
        return Ok(());
    }

    let root = std::env::args()
        .nth(1)
        .map(PathBuf::from)
        .ok_or_else(|| grit_lib::error::Error::Message("missing repository path".into()))?;
    let repo = open_repo(&root)?;

    if repo.git_dir().join("CHERRY_PICK_HEAD").exists()
        || repo.git_dir().join("REVERT_HEAD").exists()
    {
        if repo.index_handle().load()?.has_unmerged_entries() {
            println!("unmerged index — resolve and stage before continue");
            return Ok(());
        }
        match continue_replay_sequence(&repo, &replay_identity())? {
            SequenceOutcome::Completed { created, .. } => {
                println!("continued; new commits={}", created.len());
            }
            SequenceOutcome::Conflicts { conflicts, .. } => {
                println!("stopped again on {} conflict(s)", conflicts.len());
            }
            SequenceOutcome::Empty { current, .. } => {
                println!("empty step at {}", current.to_hex());
            }
        }
    } else {
        println!(
            "no replay in progress (omit path to run the built-in conflict demo, or call start_replay_sequence first)"
        );
    }

    Ok(())
}
```

The integration test `replay_sequence_git_compat` cross-checks trees, messages, and on-disk state against system `git` after `git fsck --strict`.
