diff --git a/Cargo.lock b/Cargo.lock
index 94caeaa802f..86d1618ff23 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1717,6 +1717,16 @@ dependencies = [
[[package]]
name = "git-worktree"
version = "0.0.0"
+dependencies = [
+ "git-hash 0.9.2",
+ "git-index",
+ "git-object 0.17.1",
+ "git-odb 0.26.0",
+ "git-testtools",
+ "quick-error",
+ "tempfile",
+ "walkdir",
+]
[[package]]
name = "git2"
diff --git a/git-index/src/access.rs b/git-index/src/access.rs
index 2202cdd659e..ca137e6c234 100644
--- a/git-index/src/access.rs
+++ b/git-index/src/access.rs
@@ -8,6 +8,9 @@ impl State {
pub fn entries(&self) -> &[Entry] {
&self.entries
}
+ pub fn entries_mut(&mut self) -> &mut [Entry] {
+ &mut self.entries
+ }
pub fn tree(&self) -> Option<&extension::Tree> {
self.tree.as_ref()
}
diff --git a/git-worktree/Cargo.toml b/git-worktree/Cargo.toml
index 6aa73e98b5e..abc5ce18c1b 100644
--- a/git-worktree/Cargo.toml
+++ b/git-worktree/Cargo.toml
@@ -13,3 +13,13 @@ doctest = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+git-index = { version = "^0.1.0", path = "../git-index" }
+quick-error = "2.0.1"
+git-hash = { version = "^0.9.0", path = "../git-hash" }
+git-object = { version = "^0.17.0", path = "../git-object" }
+
+[dev-dependencies]
+git-odb = { path = "../git-odb" }
+walkdir = "2.3.2"
+git-testtools = { path = "../tests/tools" }
+tempfile = "3.2.0"
diff --git a/git-worktree/src/lib.rs b/git-worktree/src/lib.rs
index d7a83e4f525..b68f45a3d80 100644
--- a/git-worktree/src/lib.rs
+++ b/git-worktree/src/lib.rs
@@ -1 +1,129 @@
#![forbid(unsafe_code, rust_2018_idioms)]
+//! Git Worktree
+
+use git_hash::oid;
+use git_object::bstr::ByteSlice;
+use quick_error::quick_error;
+use std::convert::TryFrom;
+use std::fs::{create_dir_all, OpenOptions};
+use std::io::Write;
+use std::path::{Path, PathBuf};
+use std::time::Duration;
+
+#[cfg(unix)]
+use std::os::unix::fs::OpenOptionsExt;
+
+quick_error! {
+ #[derive(Debug)]
+ pub enum Error {
+ Utf8(err: git_object::bstr::Utf8Error) {
+ from()
+ display("Could not convert path to UTF8: {}", err)
+ }
+ Time(err: std::time::SystemTimeError) {
+ from()
+ display("Could not read file time in proper format: {}", err)
+ }
+ U32Conversion(err: std::num::TryFromIntError) {
+ from()
+ display("Could not convert seconds to u32: {}", err)
+ }
+ Io(err: std::io::Error) {
+ from()
+ display("IO error while writing blob or reading file metadata or changing filetype: {}", err)
+ }
+ NotFound(oid: git_hash::ObjectId, path: PathBuf) {
+ display("unable find object of {} ({})", path.display(), oid.to_hex())
+ }
+ }
+}
+
+/// Copy index to `path`
+pub fn copy_index
(state: &mut git_index::State, path: P, mut find: Find, opts: Options) -> Result<(), Error>
+where
+ P: AsRef,
+ Find: for<'a> FnMut(&oid, &'a mut Vec) -> Option>,
+{
+ let path = path.as_ref();
+ let mut buf = Vec::new();
+ let mut entry_time = Vec::new(); // Entries whose timestamps have to be updated
+ for (i, entry) in state.entries().iter().enumerate() {
+ if entry.flags.contains(git_index::entry::Flags::SKIP_WORKTREE) {
+ continue;
+ }
+ let entry_path = entry.path(state).to_path()?;
+ let dest = path.join(entry_path);
+ create_dir_all(dest.parent().expect("entry paths are never empty"))?;
+ match entry.mode {
+ git_index::entry::Mode::FILE | git_index::entry::Mode::FILE_EXECUTABLE => {
+ let obj = find(&entry.id, &mut buf).ok_or_else(|| Error::NotFound(entry.id, path.to_path_buf()))?;
+ let mut options = OpenOptions::new();
+ options.write(true).create_new(true);
+ #[cfg(unix)]
+ if entry.mode == git_index::entry::Mode::FILE_EXECUTABLE {
+ options.mode(0o777);
+ }
+ let mut file = options.open(&dest)?;
+ file.write_all(obj.data)?;
+ let met = file.metadata()?;
+ let ctime = met
+ .created()
+ .map_or(Ok(Duration::default()), |x| x.duration_since(std::time::UNIX_EPOCH));
+ let mtime = met
+ .modified()
+ .map_or(Ok(Duration::default()), |x| x.duration_since(std::time::UNIX_EPOCH));
+ entry_time.push((ctime?, mtime?, i));
+ }
+ git_index::entry::Mode::SYMLINK => {
+ let obj = find(&entry.id, &mut buf).ok_or_else(|| Error::NotFound(entry.id, path.to_path_buf()))?;
+ let linked_to = obj.data.to_path()?;
+ if opts.symlinks {
+ #[cfg(unix)]
+ std::os::unix::fs::symlink(linked_to, &dest)?;
+ #[cfg(windows)]
+ if dest.exists() {
+ if dest.is_file() {
+ std::os::windows::fs::symlink_file(linked_to, &dest)?;
+ } else {
+ std::os::windows::fs::symlink_dir(linked_to, &dest)?;
+ }
+ }
+ } else {
+ std::fs::write(&dest, obj.data)?;
+ }
+ let met = std::fs::symlink_metadata(&dest)?;
+ let ctime = met
+ .created()
+ .map_or(Ok(Duration::default()), |x| x.duration_since(std::time::UNIX_EPOCH));
+ let mtime = met
+ .modified()
+ .map_or(Ok(Duration::default()), |x| x.duration_since(std::time::UNIX_EPOCH));
+ entry_time.push((ctime?, mtime?, i));
+ }
+ git_index::entry::Mode::DIR => todo!(),
+ git_index::entry::Mode::COMMIT => todo!(),
+ _ => unreachable!(),
+ }
+ }
+ let entries = state.entries_mut();
+ for (ctime, mtime, i) in entry_time {
+ let stat = &mut entries[i].stat;
+ stat.mtime.secs = u32::try_from(mtime.as_secs())?;
+ stat.mtime.nsecs = mtime.subsec_nanos();
+ stat.ctime.secs = u32::try_from(ctime.as_secs())?;
+ stat.ctime.nsecs = ctime.subsec_nanos();
+ }
+ Ok(())
+}
+
+/// Options for [copy_index](crate::copy_index)
+pub struct Options {
+ /// Enable/disable symlinks
+ pub symlinks: bool,
+}
+
+impl Default for Options {
+ fn default() -> Self {
+ Options { symlinks: true }
+ }
+}
diff --git a/git-worktree/tests/copy_index/mod.rs b/git-worktree/tests/copy_index/mod.rs
new file mode 100644
index 00000000000..ce8a4efd236
--- /dev/null
+++ b/git-worktree/tests/copy_index/mod.rs
@@ -0,0 +1,103 @@
+use crate::{dir_structure, fixture_path};
+use git_object::bstr::ByteSlice;
+use git_odb::FindExt;
+use git_worktree::{copy_index, Options};
+use std::fs;
+
+#[cfg(unix)]
+use std::os::unix::prelude::MetadataExt;
+
+#[test]
+fn test_copy_index() -> crate::Result<()> {
+ let path = fixture_path("make_repo");
+ let path_git = path.join(".git");
+ let mut file = git_index::File::at(path_git.join("index"), git_index::decode::Options::default())?;
+ let output_dir = tempfile::tempdir()?;
+ let output = output_dir.path();
+ let odb_handle = git_odb::at(path_git.join("objects"))?;
+
+ copy_index(
+ &mut file,
+ &output,
+ move |oid, buf| odb_handle.find_blob(oid, buf).ok(),
+ Options::default(),
+ )?;
+
+ let repo_files = dir_structure(&path);
+ let copy_files = dir_structure(output);
+
+ assert_eq!(
+ repo_files
+ .iter()
+ .flat_map(|p| p.strip_prefix(&path))
+ .collect::>(),
+ copy_files
+ .iter()
+ .flat_map(|p| p.strip_prefix(output))
+ .collect::>()
+ );
+
+ for (file1, file2) in repo_files.iter().zip(copy_files.iter()) {
+ assert_eq!(fs::read(file1)?, fs::read(file2)?);
+ #[cfg(unix)]
+ assert_eq!(
+ fs::symlink_metadata(file1)?.mode() & 0o700,
+ fs::symlink_metadata(file2)?.mode() & 0o700,
+ "Testing if the permissions (normal/executable) of {} and {} are the same",
+ file1.display(),
+ file2.display(),
+ );
+ }
+
+ Ok(())
+}
+
+#[test]
+fn test_copy_index_without_symlinks() -> crate::Result<()> {
+ let path = fixture_path("make_repo");
+ let path_git = path.join(".git");
+ let mut file = git_index::File::at(path_git.join("index"), git_index::decode::Options::default())?;
+ let output_dir = tempfile::tempdir()?;
+ let output = output_dir.path();
+ let odb_handle = git_odb::at(path_git.join("objects"))?;
+
+ copy_index(
+ &mut file,
+ &output,
+ move |oid, buf| odb_handle.find_blob(oid, buf).ok(),
+ Options { symlinks: false },
+ )?;
+
+ let repo_files = dir_structure(&path);
+ let copy_files = dir_structure(output);
+
+ assert_eq!(
+ repo_files
+ .iter()
+ .flat_map(|p| p.strip_prefix(&path))
+ .collect::>(),
+ copy_files
+ .iter()
+ .flat_map(|p| p.strip_prefix(output))
+ .collect::>()
+ );
+
+ for (file1, file2) in repo_files.iter().zip(copy_files.iter()) {
+ if file1.is_symlink() {
+ assert!(!file2.is_symlink());
+ assert_eq!(fs::read(file2)?.to_path()?, fs::read_link(file1)?);
+ } else {
+ assert_eq!(fs::read(file1)?, fs::read(file2)?);
+ #[cfg(unix)]
+ assert_eq!(
+ fs::symlink_metadata(file1)?.mode() & 0o700,
+ fs::symlink_metadata(file2)?.mode() & 0o700,
+ "Testing if the permissions (normal/executable) of {} and {} are the same",
+ file1.display(),
+ file2.display(),
+ );
+ }
+ }
+
+ Ok(())
+}
diff --git a/git-worktree/tests/fixtures/make_repo.sh b/git-worktree/tests/fixtures/make_repo.sh
new file mode 100755
index 00000000000..cc2c6ccf42d
--- /dev/null
+++ b/git-worktree/tests/fixtures/make_repo.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+set -eu -o pipefail
+
+git init -q
+git config commit.gpgsign false
+
+touch a
+echo "Test Vals" > a
+touch b
+touch c
+touch executable.sh
+chmod +x executable.sh
+
+mkdir d
+touch d/a
+echo "Subdir" > d/a
+ln -sf d/a sa
+
+git add -A
+git commit -m "Commit"
diff --git a/git-worktree/tests/mod.rs b/git-worktree/tests/mod.rs
new file mode 100644
index 00000000000..236c1fbebd0
--- /dev/null
+++ b/git-worktree/tests/mod.rs
@@ -0,0 +1,25 @@
+use std::path::{Path, PathBuf};
+use walkdir::WalkDir;
+
+mod copy_index;
+
+type Result = std::result::Result>;
+
+pub fn dir_structure>(path: P) -> Vec {
+ let path = path.as_ref();
+ let mut ps: Vec<_> = WalkDir::new(path)
+ .into_iter()
+ .filter_entry(|e| e.path() == path || !e.file_name().to_str().map(|s| s.starts_with('.')).unwrap_or(false))
+ .flatten()
+ .filter(|e| e.path().is_file())
+ .map(|p| p.path().to_path_buf())
+ .collect();
+ ps.sort();
+ ps
+}
+
+pub fn fixture_path(name: &str) -> PathBuf {
+ let dir =
+ git_testtools::scripted_fixture_repo_read_only(Path::new(name).with_extension("sh")).expect("script works");
+ dir
+}