Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -589,9 +589,10 @@ jobs:
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
SCCACHE_BUCKET: sccache-zed
- name: run_tests::check_wasm::cargo_check_wasm
run: cargo +nightly -Zbuild-std=std,panic_abort check --target wasm32-unknown-unknown -p gpui_platform
run: cargo -Zbuild-std=std,panic_abort check --target wasm32-unknown-unknown -p gpui_platform
env:
CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS: -C target-feature=+atomics,+bulk-memory,+mutable-globals
RUSTC_BOOTSTRAP: '1'
- name: steps::show_sccache_stats
run: sccache --show-stats || true
- name: steps::cleanup_cargo_config
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/action_log/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ watch.workspace = true

[dev-dependencies]
buffer_diff = { workspace = true, features = ["test-support"] }
git.workspace = true
collections = { workspace = true, features = ["test-support"] }
clock = { workspace = true, features = ["test-support"] }
ctor.workspace = true
Expand Down
135 changes: 113 additions & 22 deletions crates/action_log/src/action_log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,6 @@ impl ActionLog {
mut buffer_updates: mpsc::UnboundedReceiver<(ChangeAuthor, text::BufferSnapshot)>,
cx: &mut AsyncApp,
) -> Result<()> {
let git_store = this.read_with(cx, |this, cx| this.project.read(cx).git_store().clone())?;
let git_diff = this
.update(cx, |this, cx| {
this.project.update(cx, |project, cx| {
Expand All @@ -283,28 +282,18 @@ impl ActionLog {
})?
.await
.ok();
let buffer_repo = git_store.read_with(cx, |git_store, cx| {
git_store.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
});

let (mut git_diff_updates_tx, mut git_diff_updates_rx) = watch::channel(());
let _repo_subscription =
if let Some((git_diff, (buffer_repo, _))) = git_diff.as_ref().zip(buffer_repo) {
cx.update(|cx| {
let mut old_head = buffer_repo.read(cx).head_commit.clone();
Some(cx.subscribe(git_diff, move |_, event, cx| {
if let buffer_diff::BufferDiffEvent::DiffChanged { .. } = event {
let new_head = buffer_repo.read(cx).head_commit.clone();
if new_head != old_head {
old_head = new_head;
git_diff_updates_tx.send(()).ok();
}
}
}))
})
} else {
None
};
let _diff_subscription = if let Some(git_diff) = git_diff.as_ref() {
cx.update(|cx| {
Some(cx.subscribe(git_diff, move |_, event, _cx| {
if matches!(event, buffer_diff::BufferDiffEvent::BaseTextChanged) {
git_diff_updates_tx.send(()).ok();
}
}))
})
} else {
None
};

loop {
futures::select_biased! {
Expand Down Expand Up @@ -2714,6 +2703,108 @@ mod tests {
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
}

/// Regression test: when head_commit updates before the BufferDiff's base
/// text does, an intermediate DiffChanged (e.g. from a buffer-edit diff
/// recalculation) must NOT consume the commit signal. The subscription
/// should only fire once the base text itself has changed.
#[gpui::test]
async fn test_keep_edits_on_commit_with_stale_diff_changed(cx: &mut TestAppContext) {
init_test(cx);

let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/project"),
json!({
".git": {},
"file.txt": "aaa\nbbb\nccc\nddd\neee",
}),
)
.await;
fs.set_head_for_repo(
path!("/project/.git").as_ref(),
&[("file.txt", "aaa\nbbb\nccc\nddd\neee".into())],
"0000000",
);
cx.run_until_parked();

let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));

let file_path = project
.read_with(cx, |project, cx| {
project.find_project_path(path!("/project/file.txt"), cx)
})
.unwrap();
let buffer = project
.update(cx, |project, cx| project.open_buffer(file_path, cx))
.await
.unwrap();

// Agent makes an edit: bbb -> BBB
cx.update(|cx| {
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| {
buffer.edit([(Point::new(1, 0)..Point::new(1, 3), "BBB")], None, cx);
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
cx.run_until_parked();

// Verify the edit is tracked
let hunks = unreviewed_hunks(&action_log, cx);
assert_eq!(hunks.len(), 1);
let hunk = &hunks[0].1;
assert_eq!(hunk.len(), 1);
assert_eq!(hunk[0].old_text, "bbb\n");

// Simulate the race condition: update only the HEAD SHA first,
// without changing the committed file contents. This is analogous
// to compute_snapshot updating head_commit before
// reload_buffer_diff_bases has loaded the new base text.
fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
state.refs.insert("HEAD".into(), "0000001".into());
})
.unwrap();
cx.run_until_parked();

// Make a user edit (on a different line) to trigger a buffer diff
// recalculation. This fires DiffChanged while the BufferDiff base
// text is still the OLD text. With the old head_commit-based
// subscription this would "consume" the commit detection.
cx.update(|cx| {
buffer.update(cx, |buffer, cx| {
buffer.edit([(Point::new(3, 0)..Point::new(3, 3), "DDD")], None, cx);
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
cx.run_until_parked();

// Now update the committed file contents to match the buffer
// (the agent edit was committed). Keep the same SHA so head_commit
// does NOT change again — this is the second half of the race.
{
use git::repository::repo_path;
fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
state
.head_contents
.insert(repo_path("file.txt"), "aaa\nBBB\nccc\nDDD\neee".into());
})
.unwrap();
}
cx.run_until_parked();

// The agent's edit (bbb -> BBB) should be accepted because the
// committed content now matches. Only the user edit (ddd -> DDD)
// should remain, but since the user edit is tracked as coming from
// the user (ChangeAuthor::User) it would have been rebased into
// the diff base already. So no unreviewed hunks should remain.
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![],
"agent edits should have been accepted after the base text update"
);
}

#[gpui::test]
async fn test_undo_last_reject(cx: &mut TestAppContext) {
init_test(cx);
Expand Down
Loading
Loading