Skip to content

Commit 4177d72

Browse files
committed
Implemented git-worktree
1 parent 28e3251 commit 4177d72

File tree

7 files changed

+276
-0
lines changed

7 files changed

+276
-0
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

git-index/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ mod access {
2222
pub fn entries(&self) -> &[Entry] {
2323
&self.entries
2424
}
25+
26+
pub fn entries_mut(&mut self) -> &mut [Entry] {
27+
&mut self.entries
28+
}
2529
}
2630
}
2731

git-worktree/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,13 @@ doctest = false
1313
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1414

1515
[dependencies]
16+
git-index = { version = "^0.1.0", path = "../git-index" }
17+
quick-error = "2.0.1"
18+
git-hash = { version = "^0.9.0", path = "../git-hash" }
19+
git-object = { version = "^0.17.0", path = "../git-object" }
20+
21+
[dev-dependencies]
22+
git-odb = { path = "../git-odb" }
23+
walkdir = "2.3.2"
24+
git-testtools = { path = "../tests/tools" }
25+
tempfile = "3.2.0"

git-worktree/src/lib.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,126 @@
11
#![forbid(unsafe_code, rust_2018_idioms)]
2+
//! Git Worktree
3+
4+
use git_hash::oid;
5+
use git_object::bstr::ByteSlice;
6+
use quick_error::quick_error;
7+
use std::convert::TryFrom;
8+
use std::fs;
9+
use std::fs::create_dir_all;
10+
use std::path::Path;
11+
use std::time::Duration;
12+
13+
#[cfg(unix)]
14+
use std::os::unix::fs::PermissionsExt;
15+
16+
quick_error! {
17+
#[derive(Debug)]
18+
pub enum Error {
19+
Utf8Error(err: git_object::bstr::Utf8Error) {
20+
from()
21+
display("Could not convert path to UTF8: {}", err)
22+
}
23+
TimeError(err: std::time::SystemTimeError) {
24+
from()
25+
display("Could not read file time in proper format: {}", err)
26+
}
27+
ToU32Error(err: std::num::TryFromIntError) {
28+
from()
29+
display("Could not convert seconds to u32: {}", err)
30+
}
31+
IoError(err: std::io::Error) {
32+
from()
33+
display("IO error while writing blob or reading file metadata or changing filetype: {}", err)
34+
}
35+
FindOidError(oid: git_hash::ObjectId, path: std::path::PathBuf) {
36+
display("unable to read sha1 file of {} ({})", path.display(), oid.to_hex())
37+
}
38+
}
39+
}
40+
41+
/// Copy index to `path`
42+
pub fn copy_index<P, Find>(state: &mut git_index::State, path: P, mut find: Find, opts: Options) -> Result<(), Error>
43+
where
44+
P: AsRef<Path>,
45+
Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Option<git_object::BlobRef<'a>>,
46+
{
47+
let path = path.as_ref();
48+
let mut buf = Vec::new();
49+
let mut entry_time = Vec::new(); // Entries whose timestamps have to be updated
50+
for (i, entry) in state.entries().iter().enumerate() {
51+
if entry.flags.contains(git_index::entry::Flags::SKIP_WORKTREE) {
52+
continue;
53+
}
54+
let entry_path = entry.path(state).to_path()?;
55+
let dest = path.join(entry_path);
56+
create_dir_all(dest.parent().expect("entry paths are never empty"))?;
57+
match entry.mode {
58+
git_index::entry::Mode::FILE | git_index::entry::Mode::FILE_EXECUTABLE => {
59+
let obj = find(&entry.id, &mut buf).ok_or_else(|| Error::FindOidError(entry.id, path.to_path_buf()))?;
60+
std::fs::write(&dest, obj.data)?;
61+
if entry.mode == git_index::entry::Mode::FILE_EXECUTABLE {
62+
#[cfg(unix)]
63+
fs::set_permissions(&dest, fs::Permissions::from_mode(0o777))?;
64+
}
65+
let met = std::fs::symlink_metadata(&dest)?;
66+
let ctime = met
67+
.created()
68+
.map_or(Ok(Duration::from_secs(0)), |x| x.duration_since(std::time::UNIX_EPOCH));
69+
let mtime = met
70+
.modified()
71+
.map_or(Ok(Duration::from_secs(0)), |x| x.duration_since(std::time::UNIX_EPOCH));
72+
entry_time.push((ctime?, mtime?, i));
73+
}
74+
git_index::entry::Mode::SYMLINK => {
75+
let obj = find(&entry.id, &mut buf).unwrap();
76+
let linked_to = obj.data.to_path()?;
77+
if opts.symlinks {
78+
#[cfg(unix)]
79+
std::os::unix::fs::symlink(linked_to, &dest)?;
80+
#[cfg(windows)]
81+
if dest.exists() {
82+
if dest.is_file() {
83+
std::os::windows::fs::symlink_file(linked_to, &dest)?;
84+
} else {
85+
std::os::windows::fs::symlink_dir(linked_to, &dest)?;
86+
}
87+
}
88+
} else {
89+
std::fs::write(&dest, obj.data)?;
90+
}
91+
let met = std::fs::symlink_metadata(&dest)?;
92+
let ctime = met
93+
.created()
94+
.map_or(Ok(Duration::from_secs(0)), |x| x.duration_since(std::time::UNIX_EPOCH));
95+
let mtime = met
96+
.modified()
97+
.map_or(Ok(Duration::from_secs(0)), |x| x.duration_since(std::time::UNIX_EPOCH));
98+
entry_time.push((ctime?, mtime?, i));
99+
}
100+
git_index::entry::Mode::DIR => todo!(),
101+
git_index::entry::Mode::COMMIT => todo!(),
102+
_ => unreachable!(),
103+
}
104+
}
105+
let entries = state.entries_mut();
106+
for (ctime, mtime, i) in entry_time {
107+
let stat = &mut entries[i].stat;
108+
stat.mtime.secs = u32::try_from(mtime.as_secs())?;
109+
stat.mtime.nsecs = mtime.subsec_nanos();
110+
stat.ctime.secs = u32::try_from(ctime.as_secs())?;
111+
stat.ctime.nsecs = ctime.subsec_nanos();
112+
}
113+
Ok(())
114+
}
115+
116+
/// Options for [copy_index](crate::copy_index)
117+
pub struct Options {
118+
/// Enable/disable symlinks
119+
pub symlinks: bool,
120+
}
121+
122+
impl Default for Options {
123+
fn default() -> Self {
124+
Options { symlinks: true }
125+
}
126+
}

git-worktree/tests/copy_index/mod.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use crate::{dir_structure, fixture_path, Result};
2+
use git_object::bstr::ByteSlice;
3+
use git_odb::FindExt;
4+
use git_worktree::{copy_index, Options};
5+
use std::fs;
6+
7+
#[cfg(unix)]
8+
use std::os::unix::prelude::MetadataExt;
9+
10+
#[test]
11+
fn test_copy_index() -> Result<()> {
12+
let path = fixture_path("make_repo");
13+
let path_git = path.join(".git");
14+
let mut file = git_index::File::at(path_git.join("index"), git_index::decode::Options::default())?;
15+
let output_dir = tempfile::tempdir()?;
16+
let output = output_dir.path();
17+
let odb_handle = git_odb::at(path_git.join("objects"))?;
18+
19+
copy_index(
20+
&mut file,
21+
&output,
22+
move |oid, buf| odb_handle.find_blob(oid, buf).ok(),
23+
Options::default(),
24+
)?;
25+
26+
let repo_files = dir_structure(&path);
27+
let copy_files = dir_structure(output);
28+
29+
let srepo_files: Vec<_> = repo_files.iter().flat_map(|p| p.strip_prefix(&path)).collect();
30+
let scopy_files: Vec<_> = copy_files.iter().flat_map(|p| p.strip_prefix(output)).collect();
31+
assert_eq!(srepo_files, scopy_files);
32+
33+
for (file1, file2) in repo_files.iter().zip(copy_files.iter()) {
34+
assert_eq!(fs::read(file1)?, fs::read(file2)?);
35+
#[cfg(unix)]
36+
assert_eq!(
37+
fs::symlink_metadata(file1)?.mode() & 0b111 << 6,
38+
fs::symlink_metadata(file2)?.mode() & 0b111 << 6
39+
);
40+
}
41+
42+
Ok(())
43+
}
44+
45+
#[test]
46+
fn test_copy_index_without_symlinks() -> Result<()> {
47+
let path = fixture_path("make_repo");
48+
let path_git = path.join(".git");
49+
let mut file = git_index::File::at(path_git.join("index"), git_index::decode::Options::default())?;
50+
let output_dir = tempfile::tempdir()?;
51+
let output = output_dir.path();
52+
let odb_handle = git_odb::at(path_git.join("objects"))?;
53+
54+
copy_index(
55+
&mut file,
56+
&output,
57+
move |oid, buf| odb_handle.find_blob(oid, buf).ok(),
58+
Options { symlinks: false },
59+
)?;
60+
61+
let repo_files = dir_structure(&path);
62+
let copy_files = dir_structure(output);
63+
64+
let srepo_files: Vec<_> = repo_files.iter().flat_map(|p| p.strip_prefix(&path)).collect();
65+
let scopy_files: Vec<_> = copy_files.iter().flat_map(|p| p.strip_prefix(output)).collect();
66+
assert_eq!(srepo_files, scopy_files);
67+
68+
for (file1, file2) in repo_files.iter().zip(copy_files.iter()) {
69+
if file1.is_symlink() {
70+
assert!(!file2.is_symlink());
71+
assert_eq!(fs::read(file2)?.to_path()?, fs::read_link(file1)?);
72+
} else {
73+
assert_eq!(fs::read(file1)?, fs::read(file2)?);
74+
#[cfg(unix)]
75+
assert_eq!(
76+
fs::symlink_metadata(file1)?.mode() & 0b111 << 6,
77+
fs::symlink_metadata(file2)?.mode() & 0b111 << 6
78+
);
79+
}
80+
}
81+
82+
Ok(())
83+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/bin/bash
2+
set -eu -o pipefail
3+
4+
git init -q
5+
6+
touch a
7+
echo "Test Vals" > a
8+
touch b
9+
touch c
10+
touch executable.sh
11+
chmod +x executable.sh
12+
13+
mkdir d
14+
touch d/a
15+
echo "Subdir" > d/a
16+
ln -sf d/a sa
17+
18+
git add -A
19+
git commit -m "Commit"

git-worktree/tests/mod.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use std::path::{Path, PathBuf};
2+
use walkdir::WalkDir;
3+
4+
mod copy_index;
5+
6+
type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
7+
8+
pub fn dir_structure<P: AsRef<Path>>(path: P) -> Vec<PathBuf> {
9+
let path = path.as_ref();
10+
let mut ps: Vec<_> = WalkDir::new(path)
11+
.into_iter()
12+
.filter_entry(|e| e.path() == path || !e.file_name().to_str().map(|s| s.starts_with('.')).unwrap_or(false))
13+
.flatten()
14+
.filter(|e| e.path().is_file())
15+
.map(|p| p.path().to_path_buf())
16+
.collect();
17+
ps.sort();
18+
ps
19+
}
20+
21+
pub fn fixture_path(name: &str) -> PathBuf {
22+
let dir =
23+
git_testtools::scripted_fixture_repo_read_only(Path::new(name).with_extension("sh")).expect("script works");
24+
dir
25+
}

0 commit comments

Comments
 (0)