Skip to content

Commit 37c9edc

Browse files
fix(git): preserve staging on failed commit (#197)
Isolated fix off current `main`; includes a regression test.
1 parent cbf9100 commit 37c9edc

3 files changed

Lines changed: 105 additions & 28 deletions

File tree

src/git/versioned_store/core.rs

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -278,40 +278,67 @@ where
278278

279279
/// Commit staged changes
280280
pub fn commit(&mut self, message: &str) -> Result<gix::ObjectId, GitKvError> {
281+
let original_tree = self.tree.clone();
282+
let original_staging_area = self.staging_area.clone();
283+
281284
// Apply staged changes in a single batch so we run the streaming
282285
// canonical chunker once per commit instead of once per staged item.
283-
let changes: Vec<(Vec<u8>, Option<Vec<u8>>)> = self.staging_area.drain().collect();
286+
let changes: Vec<(Vec<u8>, Option<Vec<u8>>)> =
287+
original_staging_area.clone().into_iter().collect();
284288
self.tree.apply_changes(changes);
285289

286-
// Persist the tree state (including updating root hash and saving config)
287-
self.tree.persist_root();
288-
289-
// For all storage types, also save the tree config to git for historical access
290-
self.save_tree_config_to_git_internal()?;
291-
292-
// Get the git root directory using work_dir() for worktree/submodule compatibility
293-
let dataset_dir = self
294-
.dataset_dir
295-
.as_ref()
296-
.ok_or_else(|| GitKvError::GitObjectError("Dataset directory not set".into()))?;
297-
let git_root = self
298-
.metadata
299-
.work_dir()
300-
.or_else(|| Self::find_git_root(dataset_dir))
301-
.ok_or_else(|| GitKvError::GitObjectError("Could not find git root".into()))?;
302-
303-
// Stage and write tree via metadata backend
304-
let tree_id = self.metadata.stage_and_write_tree(&git_root)?;
305-
306-
// Create commit via metadata backend
307-
let commit_id = self.metadata.write_commit(tree_id, message)?;
290+
let commit_result = (|| {
291+
// Persist the tree state (including updating root hash and saving config)
292+
self.tree.persist_root();
293+
294+
// For all storage types, also save the tree config to git for historical access
295+
self.save_tree_config_to_git_internal()?;
296+
297+
// Get the git root directory using work_dir() for worktree/submodule compatibility
298+
let dataset_dir = self
299+
.dataset_dir
300+
.as_ref()
301+
.ok_or_else(|| GitKvError::GitObjectError("Dataset directory not set".into()))?;
302+
let git_root = self
303+
.metadata
304+
.work_dir()
305+
.or_else(|| Self::find_git_root(dataset_dir))
306+
.ok_or_else(|| GitKvError::GitObjectError("Could not find git root".into()))?;
307+
308+
// Stage and write tree via metadata backend
309+
let tree_id = self.metadata.stage_and_write_tree(&git_root)?;
310+
311+
// Create commit via metadata backend
312+
let commit_id = self.metadata.write_commit(tree_id, message)?;
313+
314+
// Update branch ref and HEAD
315+
self.metadata
316+
.update_branch(&self.current_branch, commit_id)?;
317+
self.metadata.update_head(&self.current_branch)?;
318+
319+
Ok(commit_id)
320+
})();
321+
322+
let commit_id = match commit_result {
323+
Ok(commit_id) => commit_id,
324+
Err(err) => {
325+
self.tree = original_tree;
326+
self.staging_area = original_staging_area;
327+
let staging_restore = self.save_staging_area();
328+
let _ = self.save_tree_config_to_git_internal();
329+
330+
if let Err(restore_err) = staging_restore {
331+
return Err(GitKvError::GitObjectError(format!(
332+
"{err}; additionally failed to restore staging area after aborted commit: {restore_err}"
333+
)));
334+
}
308335

309-
// Update branch ref and HEAD
310-
self.metadata
311-
.update_branch(&self.current_branch, commit_id)?;
312-
self.metadata.update_head(&self.current_branch)?;
336+
return Err(err);
337+
}
338+
};
313339

314340
// Clear staging area file since we've committed
341+
self.staging_area.clear();
315342
self.save_staging_area()?;
316343

317344
Ok(commit_id)

src/git/versioned_store/tests.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,56 @@ mod tests {
323323
assert_eq!(status.len(), 0);
324324
}
325325

326+
#[test]
327+
fn failed_commit_preserves_staging_and_committed_head() {
328+
let temp_dir = TempDir::new().unwrap();
329+
330+
gix::init(temp_dir.path()).unwrap();
331+
332+
std::process::Command::new("git")
333+
.args(["config", "user.name", "Test User"])
334+
.current_dir(temp_dir.path())
335+
.output()
336+
.expect("git config name failed");
337+
std::process::Command::new("git")
338+
.args(["config", "user.email", "test@example.com"])
339+
.current_dir(temp_dir.path())
340+
.output()
341+
.expect("git config email failed");
342+
343+
let dataset_dir = temp_dir.path().join("dataset");
344+
std::fs::create_dir_all(&dataset_dir).unwrap();
345+
let _cwd = CwdGuard::set(&dataset_dir);
346+
let mut store = GitVersionedKvStore::<32>::init(&dataset_dir).unwrap();
347+
348+
store.insert(b"key1".to_vec(), b"value1".to_vec()).unwrap();
349+
350+
let config_path = dataset_dir.join("prolly_config_tree_config");
351+
std::fs::remove_file(&config_path).unwrap();
352+
std::fs::create_dir(&config_path).unwrap();
353+
354+
let err = store
355+
.commit("commit should fail")
356+
.expect_err("config write failure should abort commit");
357+
assert!(
358+
err.to_string().contains("Failed to write config file"),
359+
"unexpected error: {err}"
360+
);
361+
362+
let status = store.status();
363+
assert_eq!(
364+
status,
365+
vec![(b"key1".to_vec(), "added".to_string())],
366+
"failed commit must leave the staged change available to retry"
367+
);
368+
369+
let head_keys = store.get_keys_at_ref("HEAD").unwrap();
370+
assert!(
371+
!head_keys.contains_key(&b"key1".to_vec()),
372+
"failed commit must not advance committed history"
373+
);
374+
}
375+
326376
#[test]
327377
fn test_single_commit_behavior() {
328378
let temp_dir = TempDir::new().unwrap();

src/tree.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ impl Default for TreeStats {
312312
}
313313
}
314314

315-
#[derive(Debug)]
315+
#[derive(Debug, Clone)]
316316
pub struct ProllyTree<const N: usize, S: NodeStorage<N>> {
317317
pub root: ProllyNode<N>,
318318
pub storage: S,

0 commit comments

Comments
 (0)