Skip to content

Commit 092e9f2

Browse files
authored
Merge pull request #2376 from staehle/user/jstaehle/fix-checkout-truncate-oss
bugfix: gix-worktree-state: Reduced-size files on checkout should be truncated to correct length
2 parents 25233ce + 69812e2 commit 092e9f2

3 files changed

Lines changed: 52 additions & 3 deletions

File tree

gix-worktree-state/src/checkout/chunk.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,10 +224,12 @@ where
224224
inner: std::io::BufWriter::with_capacity(512 * 1024, file),
225225
progress: bytes,
226226
};
227-
bytes_written += std::io::copy(&mut read, &mut write)?;
227+
let actual_bytes = std::io::copy(&mut read, &mut write)?;
228+
bytes_written += actual_bytes;
228229
entry::finalize_entry(
229230
delayed.entry,
230231
write.inner.into_inner().map_err(std::io::IntoInnerError::into_error)?,
232+
actual_bytes,
231233
set_executable_after_creation,
232234
)?;
233235
delayed_files += 1;

gix-worktree-state/src/checkout/entry.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ where
135135
};
136136

137137
// For possibly existing, overwritten files, we must change the file mode explicitly.
138-
finalize_entry(entry, file, set_executable_after_creation)?;
138+
finalize_entry(entry, file, num_bytes as u64, set_executable_after_creation)?;
139139
num_bytes
140140
}
141141
gix_index::entry::Mode::SYMLINK => {
@@ -276,19 +276,31 @@ pub(crate) fn open_file(
276276
}
277277

278278
/// Close `file` and store its stats in `entry`, possibly setting `file` executable.
279+
///
280+
/// `desired_bytes` is the amount of bytes Git thinks the file should have after writing.
279281
pub(crate) fn finalize_entry(
280282
entry: &mut gix_index::Entry,
281283
file: std::fs::File,
284+
desired_bytes: u64,
282285
#[cfg_attr(windows, allow(unused_variables))] set_executable_after_creation: bool,
283286
) -> Result<(), crate::checkout::Error> {
284287
// For possibly existing, overwritten files, we must change the file mode explicitly.
285288
#[cfg(unix)]
286289
if set_executable_after_creation {
287290
set_executable(&file)?;
288291
}
292+
293+
let md = &gix_index::fs::Metadata::from_file(&file)?;
294+
// A last sanity check: if the file wasn't truncated upon opening, which is good in case something
295+
// goes wrong during writing, not everything is lost, then after writing the file is smaller than it was
296+
// before, it needs truncation. We do that here.
297+
let needs_truncation = md.len() > desired_bytes;
298+
if needs_truncation {
299+
file.set_len(desired_bytes)?;
300+
}
289301
// NOTE: we don't call `file.sync_all()` here knowing that some filesystems don't handle this well.
290302
// revisit this once there is a bug to fix.
291-
entry.stat = Stat::from_fs(&gix_index::fs::Metadata::from_file(&file)?)?;
303+
entry.stat = Stat::from_fs(md)?;
292304
file.close()?;
293305
Ok(())
294306
}

gix-worktree-state/tests/state/checkout.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,3 +710,38 @@ fn setup_filter_pipeline(opts: &mut gix_filter::pipeline::Options) {
710710
required: true,
711711
}];
712712
}
713+
714+
#[test]
715+
fn checkout_truncates_existing_longer_files() -> crate::Result {
716+
let mut opts = opts_from_probe();
717+
opts.overwrite_existing = false;
718+
opts.destination_is_initially_empty = false;
719+
720+
// Use existing fixture and modify one file to be longer
721+
let (_source_tree, destination, _index, _outcome) = checkout_index_in_tmp_dir_opts(
722+
opts.clone(),
723+
"make_mixed_without_submodules_and_symlinks",
724+
None,
725+
|_| true,
726+
|dest| {
727+
// Create a longer version of the "executable" file before checkout
728+
let file_path = dest.join("executable");
729+
std::fs::create_dir_all(dest)?;
730+
std::fs::write(
731+
&file_path,
732+
b"This is much longer content that should be truncated to match git's version",
733+
)?;
734+
Ok(())
735+
},
736+
)?;
737+
738+
let file_path = destination.path().join("executable");
739+
let final_content = std::fs::read(&file_path)?;
740+
assert_eq!(
741+
final_content[..].as_bstr(),
742+
"content",
743+
"File content should match git's version"
744+
);
745+
746+
Ok(())
747+
}

0 commit comments

Comments
 (0)