mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Make turn diff tracker multi-env aware (#26433)
## Why Turn diffs were tracked as one flat set of absolute paths. In multi-environment turns, local and remote environments can report the same path while representing different filesystems, so a single path key can collapse distinct changes or attribute them to the wrong environment. The environment name is **NOT** included in the generated unified diff. This can come later.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
@@ -73,7 +74,6 @@ use codex_core_skills::injection::InjectedHostSkillPrompts;
|
||||
use codex_extension_api::TurnInputContext;
|
||||
use codex_extension_api::TurnInputEnvironment;
|
||||
use codex_features::Feature;
|
||||
use codex_git_utils::get_git_repo_root;
|
||||
use codex_git_utils::get_git_repo_root_with_fs;
|
||||
use codex_protocol::config_types::AutoCompactTokenLimitScope;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
@@ -196,21 +196,11 @@ pub(crate) async fn run_turn(
|
||||
let mut stop_hook_active = false;
|
||||
// Although from the perspective of codex.rs, TurnDiffTracker has the lifecycle of a Task which contains
|
||||
// many turns, from the perspective of the user, it is a single turn.
|
||||
#[allow(deprecated)]
|
||||
let display_root = match turn_context.environments.primary() {
|
||||
Some(turn_environment) => get_git_repo_root_with_fs(
|
||||
turn_environment.environment.get_filesystem().as_ref(),
|
||||
&turn_environment.cwd,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|| turn_environment.cwd.clone())
|
||||
.into_path_buf(),
|
||||
None => get_git_repo_root(turn_context.cwd.as_path())
|
||||
.unwrap_or_else(|| turn_context.cwd.clone().into_path_buf()),
|
||||
};
|
||||
let turn_diff_tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::with_display_root(
|
||||
display_root,
|
||||
)));
|
||||
let turn_diff_tracker = Arc::new(tokio::sync::Mutex::new(
|
||||
TurnDiffTracker::with_environment_display_roots(
|
||||
turn_diff_display_roots(turn_context.as_ref()).await,
|
||||
),
|
||||
));
|
||||
|
||||
// `ModelClientSession` is turn-scoped and caches WebSocket + sticky routing state, so we reuse
|
||||
// one instance across retries within this turn.
|
||||
@@ -421,6 +411,21 @@ pub(crate) async fn run_turn(
|
||||
last_agent_message
|
||||
}
|
||||
|
||||
async fn turn_diff_display_roots(turn_context: &TurnContext) -> Vec<(String, PathBuf)> {
|
||||
let mut display_roots = Vec::new();
|
||||
for turn_environment in &turn_context.environments.turn_environments {
|
||||
let root = get_git_repo_root_with_fs(
|
||||
turn_environment.environment.get_filesystem().as_ref(),
|
||||
&turn_environment.cwd,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|| turn_environment.cwd.clone())
|
||||
.into_path_buf();
|
||||
display_roots.push((turn_environment.environment_id.clone(), root));
|
||||
}
|
||||
display_roots
|
||||
}
|
||||
|
||||
async fn run_hooks_and_record_inputs(
|
||||
sess: &Arc<Session>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
|
||||
@@ -70,16 +70,25 @@ pub(crate) enum ToolEventFailure<'a> {
|
||||
}
|
||||
|
||||
enum TurnDiffTrackerUpdate<'a> {
|
||||
Track(&'a AppliedPatchDelta),
|
||||
Track {
|
||||
environment_id: Option<String>,
|
||||
delta: &'a AppliedPatchDelta,
|
||||
},
|
||||
Invalidate,
|
||||
None,
|
||||
}
|
||||
|
||||
fn tracker_update_for_known_delta(delta: &AppliedPatchDelta) -> TurnDiffTrackerUpdate<'_> {
|
||||
fn tracker_update_for_known_delta<'a>(
|
||||
environment_id: Option<&str>,
|
||||
delta: &'a AppliedPatchDelta,
|
||||
) -> TurnDiffTrackerUpdate<'a> {
|
||||
if delta.is_exact() && delta.is_empty() {
|
||||
TurnDiffTrackerUpdate::None
|
||||
} else {
|
||||
TurnDiffTrackerUpdate::Track(delta)
|
||||
TurnDiffTrackerUpdate::Track {
|
||||
environment_id: environment_id.map(str::to_string),
|
||||
delta,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +129,7 @@ pub(crate) enum ToolEmitter {
|
||||
ApplyPatch {
|
||||
changes: HashMap<PathBuf, FileChange>,
|
||||
auto_approved: bool,
|
||||
environment_id: Option<String>,
|
||||
},
|
||||
UnifiedExec {
|
||||
command: Vec<String>,
|
||||
@@ -141,10 +151,15 @@ impl ToolEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_patch(changes: HashMap<PathBuf, FileChange>, auto_approved: bool) -> Self {
|
||||
pub fn apply_patch_for_environment(
|
||||
changes: HashMap<PathBuf, FileChange>,
|
||||
auto_approved: bool,
|
||||
environment_id: String,
|
||||
) -> Self {
|
||||
Self::ApplyPatch {
|
||||
changes,
|
||||
auto_approved,
|
||||
environment_id: Some(environment_id),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +225,11 @@ impl ToolEmitter {
|
||||
.await;
|
||||
}
|
||||
(
|
||||
Self::ApplyPatch { changes, .. },
|
||||
Self::ApplyPatch {
|
||||
changes,
|
||||
environment_id,
|
||||
..
|
||||
},
|
||||
ToolEventStage::Success {
|
||||
output,
|
||||
applied_patch_delta,
|
||||
@@ -222,7 +241,7 @@ impl ToolEmitter {
|
||||
PatchApplyStatus::Failed
|
||||
};
|
||||
let tracker_update = applied_patch_delta
|
||||
.map(tracker_update_for_known_delta)
|
||||
.map(|delta| tracker_update_for_known_delta(environment_id.as_deref(), delta))
|
||||
.unwrap_or(TurnDiffTrackerUpdate::Invalidate);
|
||||
emit_patch_end(
|
||||
ctx,
|
||||
@@ -267,7 +286,11 @@ impl ToolEmitter {
|
||||
.await;
|
||||
}
|
||||
(
|
||||
Self::ApplyPatch { changes, .. },
|
||||
Self::ApplyPatch {
|
||||
changes,
|
||||
environment_id,
|
||||
..
|
||||
},
|
||||
ToolEventStage::Failure(ToolEventFailure::Rejected {
|
||||
message,
|
||||
applied_patch_delta,
|
||||
@@ -280,7 +303,9 @@ impl ToolEmitter {
|
||||
(*message).to_string(),
|
||||
PatchApplyStatus::Declined,
|
||||
applied_patch_delta
|
||||
.map(tracker_update_for_known_delta)
|
||||
.map(|delta| {
|
||||
tracker_update_for_known_delta(environment_id.as_deref(), delta)
|
||||
})
|
||||
.unwrap_or(TurnDiffTrackerUpdate::None),
|
||||
)
|
||||
.await;
|
||||
@@ -565,8 +590,11 @@ async fn emit_patch_end(
|
||||
let mut guard = tracker.lock().await;
|
||||
let previous_diff = guard.get_unified_diff();
|
||||
let tracker_changed = match tracker_update {
|
||||
TurnDiffTrackerUpdate::Track(delta) => {
|
||||
guard.track_delta(delta);
|
||||
TurnDiffTrackerUpdate::Track {
|
||||
environment_id,
|
||||
delta,
|
||||
} => {
|
||||
guard.track_delta(environment_id.as_deref().unwrap_or_default(), delta);
|
||||
true
|
||||
}
|
||||
TurnDiffTrackerUpdate::Invalidate => {
|
||||
@@ -627,14 +655,18 @@ mod tests {
|
||||
.await
|
||||
.expect("apply patch");
|
||||
|
||||
ToolEmitter::apply_patch(HashMap::new(), /*auto_approved*/ false)
|
||||
.finish(
|
||||
ToolEventCtx::new(session.as_ref(), turn.as_ref(), "call-id", Some(&tracker)),
|
||||
out,
|
||||
Some(&delta),
|
||||
)
|
||||
.await
|
||||
.expect_err("failed patch");
|
||||
ToolEmitter::ApplyPatch {
|
||||
changes: HashMap::new(),
|
||||
auto_approved: false,
|
||||
environment_id: None,
|
||||
}
|
||||
.finish(
|
||||
ToolEventCtx::new(session.as_ref(), turn.as_ref(), "call-id", Some(&tracker)),
|
||||
out,
|
||||
Some(&delta),
|
||||
)
|
||||
.await
|
||||
.expect_err("failed patch");
|
||||
|
||||
let completed = rx_event.recv().await.expect("item completed event");
|
||||
assert!(matches!(
|
||||
|
||||
@@ -378,8 +378,11 @@ impl ToolExecutor<ToolInvocation> for ApplyPatchHandler {
|
||||
}
|
||||
InternalApplyPatchInvocation::DelegateToRuntime(apply) => {
|
||||
let changes = convert_apply_patch_to_protocol(&apply.action);
|
||||
let emitter =
|
||||
ToolEmitter::apply_patch(changes.clone(), apply.auto_approved);
|
||||
let emitter = ToolEmitter::apply_patch_for_environment(
|
||||
changes.clone(),
|
||||
apply.auto_approved,
|
||||
turn_environment.environment_id.clone(),
|
||||
);
|
||||
let event_ctx = ToolEventCtx::new(
|
||||
session.as_ref(),
|
||||
turn.as_ref(),
|
||||
@@ -537,7 +540,11 @@ pub(crate) async fn intercept_apply_patch(
|
||||
}
|
||||
InternalApplyPatchInvocation::DelegateToRuntime(apply) => {
|
||||
let changes = convert_apply_patch_to_protocol(&apply.action);
|
||||
let emitter = ToolEmitter::apply_patch(changes.clone(), apply.auto_approved);
|
||||
let emitter = ToolEmitter::apply_patch_for_environment(
|
||||
changes.clone(),
|
||||
apply.auto_approved,
|
||||
turn_environment.environment_id.clone(),
|
||||
);
|
||||
let event_ctx = ToolEventCtx::new(
|
||||
session.as_ref(),
|
||||
turn.as_ref(),
|
||||
|
||||
@@ -13,21 +13,36 @@ const ZERO_OID: &str = "0000000000000000000000000000000000000000";
|
||||
const DEV_NULL: &str = "/dev/null";
|
||||
const REGULAR_FILE_MODE: &str = "100644";
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
struct TrackedPath {
|
||||
environment_id: String,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl TrackedPath {
|
||||
fn new(environment_id: &str, path: &Path) -> Self {
|
||||
Self {
|
||||
environment_id: environment_id.to_string(),
|
||||
path: path.to_path_buf(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks the net text diff for the current turn from committed apply_patch
|
||||
/// mutations, without rereading the workspace filesystem.
|
||||
pub struct TurnDiffTracker {
|
||||
valid: bool,
|
||||
display_root: Option<PathBuf>,
|
||||
baseline_by_path: HashMap<PathBuf, String>,
|
||||
current_by_path: HashMap<PathBuf, String>,
|
||||
origin_by_current_path: HashMap<PathBuf, PathBuf>,
|
||||
display_roots_by_environment: HashMap<String, PathBuf>,
|
||||
baseline_by_path: HashMap<TrackedPath, String>,
|
||||
current_by_path: HashMap<TrackedPath, String>,
|
||||
origin_by_current_path: HashMap<TrackedPath, TrackedPath>,
|
||||
}
|
||||
|
||||
impl Default for TurnDiffTracker {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
valid: true,
|
||||
display_root: None,
|
||||
display_roots_by_environment: HashMap::new(),
|
||||
baseline_by_path: HashMap::new(),
|
||||
current_by_path: HashMap::new(),
|
||||
origin_by_current_path: HashMap::new(),
|
||||
@@ -40,20 +55,22 @@ impl TurnDiffTracker {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_display_root(display_root: PathBuf) -> Self {
|
||||
pub fn with_environment_display_roots(
|
||||
display_roots: impl IntoIterator<Item = (String, PathBuf)>,
|
||||
) -> Self {
|
||||
let mut tracker = Self::new();
|
||||
tracker.display_root = Some(display_root);
|
||||
tracker.display_roots_by_environment = display_roots.into_iter().collect();
|
||||
tracker
|
||||
}
|
||||
|
||||
pub fn track_delta(&mut self, delta: &AppliedPatchDelta) {
|
||||
pub fn track_delta(&mut self, environment_id: &str, delta: &AppliedPatchDelta) {
|
||||
if !delta.is_exact() {
|
||||
self.invalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
for change in delta.changes() {
|
||||
self.apply_change(change);
|
||||
self.apply_change(environment_id, change);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,8 +123,8 @@ impl TurnDiffTracker {
|
||||
(!aggregated.is_empty()).then_some(aggregated)
|
||||
}
|
||||
|
||||
fn apply_change(&mut self, change: &AppliedPatchChange) {
|
||||
let source_path = change.path.as_path();
|
||||
fn apply_change(&mut self, environment_id: &str, change: &AppliedPatchChange) {
|
||||
let source_path = TrackedPath::new(environment_id, change.path.as_path());
|
||||
match &change.change {
|
||||
AppliedPatchFileChange::Add {
|
||||
content,
|
||||
@@ -119,85 +136,87 @@ impl TurnDiffTracker {
|
||||
old_content,
|
||||
overwritten_move_content,
|
||||
new_content,
|
||||
} => self.apply_update(
|
||||
source_path,
|
||||
move_path.as_deref(),
|
||||
old_content,
|
||||
overwritten_move_content.as_deref(),
|
||||
new_content,
|
||||
),
|
||||
} => {
|
||||
let move_path = move_path
|
||||
.as_deref()
|
||||
.map(|path| TrackedPath::new(environment_id, path));
|
||||
self.apply_update(
|
||||
source_path,
|
||||
move_path,
|
||||
old_content,
|
||||
overwritten_move_content.as_deref(),
|
||||
new_content,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_add(&mut self, path: &Path, content: &str, overwritten_content: Option<&str>) {
|
||||
self.origin_by_current_path.remove(path);
|
||||
if !self.current_by_path.contains_key(path)
|
||||
&& !self.baseline_by_path.contains_key(path)
|
||||
fn apply_add(&mut self, path: TrackedPath, content: &str, overwritten_content: Option<&str>) {
|
||||
self.origin_by_current_path.remove(&path);
|
||||
if !self.current_by_path.contains_key(&path)
|
||||
&& !self.baseline_by_path.contains_key(&path)
|
||||
&& let Some(overwritten_content) = overwritten_content
|
||||
{
|
||||
self.baseline_by_path
|
||||
.insert(path.to_path_buf(), overwritten_content.to_string());
|
||||
.insert(path.clone(), overwritten_content.to_string());
|
||||
}
|
||||
self.current_by_path
|
||||
.insert(path.to_path_buf(), content.to_string());
|
||||
self.current_by_path.insert(path, content.to_string());
|
||||
}
|
||||
|
||||
fn apply_delete(&mut self, path: &Path, content: &str) {
|
||||
if self.current_by_path.remove(path).is_none() && !self.baseline_by_path.contains_key(path)
|
||||
fn apply_delete(&mut self, path: TrackedPath, content: &str) {
|
||||
if self.current_by_path.remove(&path).is_none()
|
||||
&& !self.baseline_by_path.contains_key(&path)
|
||||
{
|
||||
self.baseline_by_path
|
||||
.insert(path.to_path_buf(), content.to_string());
|
||||
.insert(path.clone(), content.to_string());
|
||||
}
|
||||
self.origin_by_current_path.remove(path);
|
||||
self.origin_by_current_path.remove(&path);
|
||||
}
|
||||
|
||||
fn apply_update(
|
||||
&mut self,
|
||||
source_path: &Path,
|
||||
move_path: Option<&Path>,
|
||||
source_path: TrackedPath,
|
||||
move_path: Option<TrackedPath>,
|
||||
old_content: &str,
|
||||
overwritten_move_content: Option<&str>,
|
||||
new_content: &str,
|
||||
) {
|
||||
if !self.current_by_path.contains_key(source_path)
|
||||
&& !self.baseline_by_path.contains_key(source_path)
|
||||
if !self.current_by_path.contains_key(&source_path)
|
||||
&& !self.baseline_by_path.contains_key(&source_path)
|
||||
{
|
||||
self.baseline_by_path
|
||||
.insert(source_path.to_path_buf(), old_content.to_string());
|
||||
.insert(source_path.clone(), old_content.to_string());
|
||||
}
|
||||
|
||||
match move_path {
|
||||
Some(dest_path) => {
|
||||
if !self.current_by_path.contains_key(dest_path)
|
||||
&& !self.baseline_by_path.contains_key(dest_path)
|
||||
if !self.current_by_path.contains_key(&dest_path)
|
||||
&& !self.baseline_by_path.contains_key(&dest_path)
|
||||
&& let Some(overwritten_move_content) = overwritten_move_content
|
||||
{
|
||||
self.baseline_by_path.insert(
|
||||
dest_path.to_path_buf(),
|
||||
overwritten_move_content.to_string(),
|
||||
);
|
||||
self.baseline_by_path
|
||||
.insert(dest_path.clone(), overwritten_move_content.to_string());
|
||||
}
|
||||
let origin = self
|
||||
.origin_by_current_path
|
||||
.remove(source_path)
|
||||
.unwrap_or_else(|| source_path.to_path_buf());
|
||||
self.current_by_path.remove(source_path);
|
||||
.remove(&source_path)
|
||||
.unwrap_or_else(|| source_path.clone());
|
||||
self.current_by_path.remove(&source_path);
|
||||
self.current_by_path
|
||||
.insert(dest_path.to_path_buf(), new_content.to_string());
|
||||
self.origin_by_current_path.remove(dest_path);
|
||||
if dest_path != origin.as_path() {
|
||||
self.origin_by_current_path
|
||||
.insert(dest_path.to_path_buf(), origin);
|
||||
.insert(dest_path.clone(), new_content.to_string());
|
||||
self.origin_by_current_path.remove(&dest_path);
|
||||
if dest_path != origin {
|
||||
self.origin_by_current_path.insert(dest_path, origin);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.current_by_path
|
||||
.insert(source_path.to_path_buf(), new_content.to_string());
|
||||
.insert(source_path, new_content.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rename_pairs(&self) -> HashMap<PathBuf, PathBuf> {
|
||||
fn rename_pairs(&self) -> HashMap<TrackedPath, TrackedPath> {
|
||||
self.origin_by_current_path
|
||||
.iter()
|
||||
.filter_map(|(dest_path, origin_path)| {
|
||||
@@ -215,7 +234,7 @@ impl TurnDiffTracker {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_path_diff(&self, path: &Path) -> Option<String> {
|
||||
fn render_path_diff(&self, path: &TrackedPath) -> Option<String> {
|
||||
self.render_diff(
|
||||
path,
|
||||
self.baseline_by_path.get(path).map(String::as_str),
|
||||
@@ -224,7 +243,11 @@ impl TurnDiffTracker {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_rename_diff(&self, source_path: &Path, dest_path: &Path) -> Option<String> {
|
||||
fn render_rename_diff(
|
||||
&self,
|
||||
source_path: &TrackedPath,
|
||||
dest_path: &TrackedPath,
|
||||
) -> Option<String> {
|
||||
self.render_diff(
|
||||
source_path,
|
||||
self.baseline_by_path.get(source_path).map(String::as_str),
|
||||
@@ -235,9 +258,9 @@ impl TurnDiffTracker {
|
||||
|
||||
fn render_diff(
|
||||
&self,
|
||||
left_path: &Path,
|
||||
left_path: &TrackedPath,
|
||||
left_content: Option<&str>,
|
||||
right_path: &Path,
|
||||
right_path: &TrackedPath,
|
||||
right_content: Option<&str>,
|
||||
) -> Option<String> {
|
||||
if left_content == right_content {
|
||||
@@ -286,13 +309,18 @@ impl TurnDiffTracker {
|
||||
Some(diff)
|
||||
}
|
||||
|
||||
fn display_path(&self, path: &Path) -> String {
|
||||
fn display_path(&self, path: &TrackedPath) -> String {
|
||||
let display = self
|
||||
.display_root
|
||||
.as_deref()
|
||||
.and_then(|root| path.strip_prefix(root).ok())
|
||||
.unwrap_or(path);
|
||||
display.display().to_string().replace('\\', "/")
|
||||
.display_roots_by_environment
|
||||
.get(&path.environment_id)
|
||||
.and_then(|root| path.path.strip_prefix(root).ok())
|
||||
.unwrap_or(path.path.as_path());
|
||||
let display = display.display().to_string().replace('\\', "/");
|
||||
if self.display_roots_by_environment.len() > 1 && !path.environment_id.is_empty() {
|
||||
format!("{}/{display}", path.environment_id)
|
||||
} else {
|
||||
display
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,24 +41,28 @@ async fn apply_verified_patch(root: &Path, patch: &str) -> AppliedPatchDelta {
|
||||
.expect("patch should apply")
|
||||
}
|
||||
|
||||
fn tracker_with_root(root: &Path) -> TurnDiffTracker {
|
||||
TurnDiffTracker::with_environment_display_roots([("".to_string(), root.to_path_buf())])
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn accumulates_add_then_update_as_single_add() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let mut tracker = TurnDiffTracker::with_display_root(dir.path().to_path_buf());
|
||||
let mut tracker = tracker_with_root(dir.path());
|
||||
|
||||
let add = apply_verified_patch(
|
||||
dir.path(),
|
||||
"*** Begin Patch\n*** Add File: a.txt\n+foo\n*** End Patch",
|
||||
)
|
||||
.await;
|
||||
tracker.track_delta(&add);
|
||||
tracker.track_delta("", &add);
|
||||
|
||||
let update = apply_verified_patch(
|
||||
dir.path(),
|
||||
"*** Begin Patch\n*** Update File: a.txt\n@@\n foo\n+bar\n*** End Patch",
|
||||
)
|
||||
.await;
|
||||
tracker.track_delta(&update);
|
||||
tracker.track_delta("", &update);
|
||||
|
||||
let right_oid = git_blob_sha1_hex("foo\nbar\n");
|
||||
let expected = format!(
|
||||
@@ -78,32 +82,69 @@ index {ZERO_OID}..{right_oid}
|
||||
#[tokio::test]
|
||||
async fn invalidated_tracker_suppresses_existing_diff() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let mut tracker = TurnDiffTracker::with_display_root(dir.path().to_path_buf());
|
||||
let mut tracker = tracker_with_root(dir.path());
|
||||
|
||||
let add = apply_verified_patch(
|
||||
dir.path(),
|
||||
"*** Begin Patch\n*** Add File: a.txt\n+foo\n*** End Patch",
|
||||
)
|
||||
.await;
|
||||
tracker.track_delta(&add);
|
||||
tracker.track_delta("", &add);
|
||||
|
||||
tracker.invalidate();
|
||||
|
||||
assert_eq!(tracker.get_unified_diff(), None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tracks_same_absolute_path_across_multiple_environments() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let add = apply_verified_patch(
|
||||
dir.path(),
|
||||
"*** Begin Patch\n*** Add File: shared.txt\n+content\n*** End Patch",
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut tracker = TurnDiffTracker::with_environment_display_roots([
|
||||
("local".to_string(), dir.path().to_path_buf()),
|
||||
("remote".to_string(), dir.path().to_path_buf()),
|
||||
]);
|
||||
tracker.track_delta("remote", &add);
|
||||
tracker.track_delta("local", &add);
|
||||
|
||||
let right_oid = git_blob_sha1_hex("content\n");
|
||||
let expected = format!(
|
||||
r#"diff --git a/local/shared.txt b/local/shared.txt
|
||||
new file mode {REGULAR_FILE_MODE}
|
||||
index {ZERO_OID}..{right_oid}
|
||||
--- {DEV_NULL}
|
||||
+++ b/local/shared.txt
|
||||
@@ -0,0 +1 @@
|
||||
+content
|
||||
diff --git a/remote/shared.txt b/remote/shared.txt
|
||||
new file mode {REGULAR_FILE_MODE}
|
||||
index {ZERO_OID}..{right_oid}
|
||||
--- {DEV_NULL}
|
||||
+++ b/remote/shared.txt
|
||||
@@ -0,0 +1 @@
|
||||
+content
|
||||
"#,
|
||||
);
|
||||
assert_eq!(tracker.get_unified_diff(), Some(expected));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn accumulates_delete() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
fs::write(dir.path().join("b.txt"), "x\n").expect("seed file");
|
||||
|
||||
let mut tracker = TurnDiffTracker::with_display_root(dir.path().to_path_buf());
|
||||
let mut tracker = tracker_with_root(dir.path());
|
||||
let delete = apply_verified_patch(
|
||||
dir.path(),
|
||||
"*** Begin Patch\n*** Delete File: b.txt\n*** End Patch",
|
||||
)
|
||||
.await;
|
||||
tracker.track_delta(&delete);
|
||||
tracker.track_delta("", &delete);
|
||||
|
||||
let left_oid = git_blob_sha1_hex("x\n");
|
||||
let expected = format!(
|
||||
@@ -124,13 +165,13 @@ async fn accumulates_move_and_update() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
fs::write(dir.path().join("src.txt"), "line\n").expect("seed file");
|
||||
|
||||
let mut tracker = TurnDiffTracker::with_display_root(dir.path().to_path_buf());
|
||||
let mut tracker = tracker_with_root(dir.path());
|
||||
let update = apply_verified_patch(
|
||||
dir.path(),
|
||||
"*** Begin Patch\n*** Update File: src.txt\n*** Move to: dst.txt\n@@\n-line\n+line2\n*** End Patch",
|
||||
)
|
||||
.await;
|
||||
tracker.track_delta(&update);
|
||||
tracker.track_delta("", &update);
|
||||
|
||||
let left_oid = git_blob_sha1_hex("line\n");
|
||||
let right_oid = git_blob_sha1_hex("line2\n");
|
||||
@@ -152,13 +193,13 @@ async fn pure_rename_yields_no_diff() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
fs::write(dir.path().join("old.txt"), "same\n").expect("seed file");
|
||||
|
||||
let mut tracker = TurnDiffTracker::with_display_root(dir.path().to_path_buf());
|
||||
let mut tracker = tracker_with_root(dir.path());
|
||||
let rename = apply_verified_patch(
|
||||
dir.path(),
|
||||
"*** Begin Patch\n*** Update File: old.txt\n*** Move to: new.txt\n@@\n same\n*** End Patch",
|
||||
)
|
||||
.await;
|
||||
tracker.track_delta(&rename);
|
||||
tracker.track_delta("", &rename);
|
||||
|
||||
assert_eq!(tracker.get_unified_diff(), None);
|
||||
}
|
||||
@@ -168,13 +209,13 @@ async fn add_over_existing_file_becomes_update() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
fs::write(dir.path().join("dup.txt"), "before\n").expect("seed file");
|
||||
|
||||
let mut tracker = TurnDiffTracker::with_display_root(dir.path().to_path_buf());
|
||||
let mut tracker = tracker_with_root(dir.path());
|
||||
let add = apply_verified_patch(
|
||||
dir.path(),
|
||||
"*** Begin Patch\n*** Add File: dup.txt\n+after\n*** End Patch",
|
||||
)
|
||||
.await;
|
||||
tracker.track_delta(&add);
|
||||
tracker.track_delta("", &add);
|
||||
|
||||
let left_oid = git_blob_sha1_hex("before\n");
|
||||
let right_oid = git_blob_sha1_hex("after\n");
|
||||
@@ -196,20 +237,20 @@ async fn delete_then_readd_same_path_becomes_update() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
fs::write(dir.path().join("cycle.txt"), "before\n").expect("seed file");
|
||||
|
||||
let mut tracker = TurnDiffTracker::with_display_root(dir.path().to_path_buf());
|
||||
let mut tracker = tracker_with_root(dir.path());
|
||||
let delete = apply_verified_patch(
|
||||
dir.path(),
|
||||
"*** Begin Patch\n*** Delete File: cycle.txt\n*** End Patch",
|
||||
)
|
||||
.await;
|
||||
tracker.track_delta(&delete);
|
||||
tracker.track_delta("", &delete);
|
||||
|
||||
let add = apply_verified_patch(
|
||||
dir.path(),
|
||||
"*** Begin Patch\n*** Add File: cycle.txt\n+after\n*** End Patch",
|
||||
)
|
||||
.await;
|
||||
tracker.track_delta(&add);
|
||||
tracker.track_delta("", &add);
|
||||
|
||||
let left_oid = git_blob_sha1_hex("before\n");
|
||||
let right_oid = git_blob_sha1_hex("after\n");
|
||||
@@ -232,13 +273,13 @@ async fn move_over_existing_destination_without_content_change_deletes_source_on
|
||||
fs::write(dir.path().join("a.txt"), "same\n").expect("seed source");
|
||||
fs::write(dir.path().join("b.txt"), "same\n").expect("seed destination");
|
||||
|
||||
let mut tracker = TurnDiffTracker::with_display_root(dir.path().to_path_buf());
|
||||
let mut tracker = tracker_with_root(dir.path());
|
||||
let move_overwrite = apply_verified_patch(
|
||||
dir.path(),
|
||||
"*** Begin Patch\n*** Update File: a.txt\n*** Move to: b.txt\n@@\n same\n*** End Patch",
|
||||
)
|
||||
.await;
|
||||
tracker.track_delta(&move_overwrite);
|
||||
tracker.track_delta("", &move_overwrite);
|
||||
|
||||
let left_oid = git_blob_sha1_hex("same\n");
|
||||
let expected = format!(
|
||||
@@ -261,13 +302,13 @@ async fn move_over_existing_destination_with_content_change_deletes_source_and_u
|
||||
fs::write(dir.path().join("a.txt"), "from\n").expect("seed source");
|
||||
fs::write(dir.path().join("b.txt"), "existing\n").expect("seed destination");
|
||||
|
||||
let mut tracker = TurnDiffTracker::with_display_root(dir.path().to_path_buf());
|
||||
let mut tracker = tracker_with_root(dir.path());
|
||||
let move_overwrite = apply_verified_patch(
|
||||
dir.path(),
|
||||
"*** Begin Patch\n*** Update File: a.txt\n*** Move to: b.txt\n@@\n-from\n+new\n*** End Patch",
|
||||
)
|
||||
.await;
|
||||
tracker.track_delta(&move_overwrite);
|
||||
tracker.track_delta("", &move_overwrite);
|
||||
|
||||
let left_oid_a = git_blob_sha1_hex("from\n");
|
||||
let left_oid_b = git_blob_sha1_hex("existing\n");
|
||||
@@ -298,13 +339,13 @@ async fn preserves_committed_change_order_with_delete_then_move_overwrite() {
|
||||
fs::write(dir.path().join("a.txt"), "from\n").expect("seed source");
|
||||
fs::write(dir.path().join("b.txt"), "existing\n").expect("seed destination");
|
||||
|
||||
let mut tracker = TurnDiffTracker::with_display_root(dir.path().to_path_buf());
|
||||
let mut tracker = tracker_with_root(dir.path());
|
||||
let ordered_patch = apply_verified_patch(
|
||||
dir.path(),
|
||||
"*** Begin Patch\n*** Delete File: b.txt\n*** Update File: a.txt\n*** Move to: b.txt\n@@\n-from\n+new\n*** End Patch",
|
||||
)
|
||||
.await;
|
||||
tracker.track_delta(&ordered_patch);
|
||||
tracker.track_delta("", &ordered_patch);
|
||||
|
||||
let left_oid_a = git_blob_sha1_hex("from\n");
|
||||
let left_oid_b = git_blob_sha1_hex("existing\n");
|
||||
|
||||
Reference in New Issue
Block a user