Skip to content

Commit f438ac2

Browse files
committed
commit_builder: error out if newly-created commit was already known to repo
This can happen if we remove predecessors from the commit object, and if committer timestamp wasn't updated (like in some jj-lib tests.) It's not wrong for the store to "overwrite" existing objects, but the repo expects new/rewritten commits have unique ids. If they didn't, cycle would be created in mut_repo.commit_predecessors/parent_mapping.
1 parent 3023d1c commit f438ac2

File tree

3 files changed

+67
-1
lines changed

3 files changed

+67
-1
lines changed

lib/src/commit_builder.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use std::sync::Arc;
1919
use pollster::FutureExt as _;
2020

2121
use crate::backend;
22+
use crate::backend::BackendError;
2223
use crate::backend::BackendResult;
2324
use crate::backend::ChangeId;
2425
use crate::backend::CommitId;
@@ -351,6 +352,15 @@ impl DetachedCommitBuilder {
351352
pub fn write(self, mut_repo: &mut MutableRepo) -> BackendResult<Commit> {
352353
let predecessors = self.commit.predecessors.clone();
353354
let commit = write_to_store(&self.store, self.commit, &self.sign_settings)?;
355+
// FIXME: Google's index.has_id() always returns true.
356+
if mut_repo.is_backed_by_default_index() && mut_repo.index().has_id(commit.id()) {
357+
// Recording existing commit as new would create cycle in
358+
// predecessors/parent mappings within the current transaction, and
359+
// in predecessors graph globally.
360+
return Err(BackendError::Other(
361+
format!("Newly-created commit {id} already exists", id = commit.id()).into(),
362+
));
363+
}
354364
mut_repo.add_head(&commit)?;
355365
mut_repo.set_predecessors(commit.id().clone(), predecessors);
356366
if let Some(rewrite_source) = self.rewrite_source {

lib/src/repo.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,10 @@ impl MutableRepo {
900900
self.index.as_ref()
901901
}
902902

903+
pub(crate) fn is_backed_by_default_index(&self) -> bool {
904+
self.index.as_any().is::<DefaultMutableIndex>()
905+
}
906+
903907
pub fn has_changes(&self) -> bool {
904908
self.view.ensure_clean(|v| self.enforce_view_invariants(v));
905909
!(self.commit_predecessors.is_empty()
@@ -1781,7 +1785,7 @@ impl MutableRepo {
17811785
// feasible to walk all added and removed commits.
17821786
// TODO: Fix this somehow. Maybe a method on `Index` to find rewritten commits
17831787
// given `base_heads`, `own_heads` and `other_heads`?
1784-
if self.index.as_any().is::<DefaultMutableIndex>() {
1788+
if self.is_backed_by_default_index() {
17851789
self.record_rewrites(&base_heads, &own_heads)?;
17861790
self.record_rewrites(&base_heads, &other_heads)?;
17871791
// No need to remove heads removed by `other` because we already

lib/tests/test_commit_builder.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use assert_matches::assert_matches;
1616
use futures::StreamExt as _;
1717
use indoc::indoc;
1818
use itertools::Itertools as _;
19+
use jj_lib::backend::BackendError;
1920
use jj_lib::backend::ChangeId;
2021
use jj_lib::backend::MillisSinceEpoch;
2122
use jj_lib::backend::Signature;
@@ -367,6 +368,57 @@ fn test_rewrite_resets_author_timestamp(backend: TestRepoBackend) {
367368
assert_eq!(rewritten_commit_2.committer().timestamp, new_timestamp_2);
368369
}
369370

371+
#[test_case(TestRepoBackend::Simple ; "simple backend")]
372+
#[test_case(TestRepoBackend::Git ; "git backend")]
373+
fn test_rewrite_to_identical_commit(backend: TestRepoBackend) {
374+
let timestamp = "2001-02-03T04:05:06+07:00";
375+
let settings = UserSettings::from_config(config_with_commit_timestamp(timestamp)).unwrap();
376+
let test_repo = TestRepo::init_with_backend_and_settings(backend, &settings);
377+
let repo = test_repo.repo;
378+
let store = repo.store();
379+
380+
let mut tx = repo.start_transaction();
381+
let commit1 = tx
382+
.repo_mut()
383+
.new_commit(
384+
vec![store.root_commit_id().clone()],
385+
store.empty_merged_tree_id(),
386+
)
387+
.write()
388+
.unwrap();
389+
let repo = tx.commit("test").unwrap();
390+
391+
// Create commit identical to the original
392+
let mut tx = repo.start_transaction();
393+
let mut builder = tx.repo_mut().rewrite_commit(&commit1).detach();
394+
builder.set_predecessors(vec![]);
395+
// Writing to the store should work
396+
let commit2 = builder.write_hidden().unwrap();
397+
assert_eq!(commit1, commit2);
398+
// Writing to the repo shouldn't work, which would create cycle in
399+
// predecessors/parent mappings
400+
let result = builder.write(tx.repo_mut());
401+
assert_matches!(result, Err(BackendError::Other(_)));
402+
tx.repo_mut().rebase_descendants().unwrap();
403+
tx.commit("test").unwrap();
404+
405+
// Create two rewritten commits of the same content and metadata
406+
let mut tx = repo.start_transaction();
407+
tx.repo_mut()
408+
.rewrite_commit(&commit1)
409+
.set_description("rewritten")
410+
.write()
411+
.unwrap();
412+
let result = tx
413+
.repo_mut()
414+
.rewrite_commit(&commit1)
415+
.set_description("rewritten")
416+
.write();
417+
assert_matches!(result, Err(BackendError::Other(_)));
418+
tx.repo_mut().rebase_descendants().unwrap();
419+
tx.commit("test").unwrap();
420+
}
421+
370422
#[test_case(TestRepoBackend::Simple ; "simple backend")]
371423
// #[test_case(TestRepoBackend::Git ; "git backend")]
372424
fn test_commit_builder_descendants(backend: TestRepoBackend) {

0 commit comments

Comments
 (0)