Skip to content

Commit 91c064e

Browse files
committed
Implemented git-worktree
1 parent 28e3251 commit 91c064e

File tree

7 files changed

+256
-0
lines changed

7 files changed

+256
-0
lines changed

Cargo.lock

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

git-index/src/lib.rs

+4
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

+11
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,14 @@ 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"
26+
anyhow = "1.0.42"

git-worktree/src/lib.rs

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

git-worktree/tests/copy_index/mod.rs

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use crate::{dir_structure, fixture_path};
2+
use anyhow::Result;
3+
use git_object::bstr::ByteSlice;
4+
use git_odb::FindExt;
5+
use git_worktree::{copy_index, Options};
6+
use std::fs;
7+
8+
#[cfg(unix)]
9+
use std::os::unix::prelude::MetadataExt;
10+
11+
#[test]
12+
fn test_copy_index() -> Result<()> {
13+
let path = fixture_path("make_repo");
14+
let path_git = path.join(".git");
15+
let mut file = git_index::File::at(path_git.join("index"), git_index::decode::Options::default())?;
16+
let output_dir = tempfile::tempdir()?;
17+
let output = output_dir.path();
18+
let odb_handle = git_odb::at(path_git.join("objects")).unwrap();
19+
20+
copy_index(
21+
&mut file,
22+
&output,
23+
move |oid, buf| odb_handle.find(oid, buf).ok(),
24+
Options::default(),
25+
)?;
26+
27+
let repo_files = dir_structure(&path);
28+
let copy_files = dir_structure(output);
29+
30+
let srepo_files: Vec<_> = repo_files.iter().flat_map(|p| p.strip_prefix(&path)).collect();
31+
let scopy_files: Vec<_> = copy_files.iter().flat_map(|p| p.strip_prefix(output)).collect();
32+
assert!(srepo_files == scopy_files);
33+
34+
for (file1, file2) in repo_files.iter().zip(copy_files.iter()) {
35+
assert!(fs::read(file1)? == fs::read(file2)?);
36+
#[cfg(unix)]
37+
assert!(fs::metadata(file1)?.mode() & 0b111 << 6 == fs::metadata(file2)?.mode() & 0b111 << 6);
38+
}
39+
40+
Ok(())
41+
}
42+
43+
#[test]
44+
fn test_copy_index_without_symlinks() -> Result<()> {
45+
let path = fixture_path("make_repo");
46+
let path_git = path.join(".git");
47+
let mut file = git_index::File::at(path_git.join("index"), git_index::decode::Options::default())?;
48+
let output_dir = tempfile::tempdir()?;
49+
let output = output_dir.path();
50+
let odb_handle = git_odb::at(path_git.join("objects")).unwrap();
51+
52+
copy_index(
53+
&mut file,
54+
&output,
55+
move |oid, buf| odb_handle.find(oid, buf).ok(),
56+
Options { symlinks: false },
57+
)?;
58+
59+
let repo_files = dir_structure(&path);
60+
let copy_files = dir_structure(output);
61+
62+
let srepo_files: Vec<_> = repo_files.iter().flat_map(|p| p.strip_prefix(&path)).collect();
63+
let scopy_files: Vec<_> = copy_files.iter().flat_map(|p| p.strip_prefix(output)).collect();
64+
assert!(srepo_files == scopy_files);
65+
66+
for (file1, file2) in repo_files.iter().zip(copy_files.iter()) {
67+
if file1.is_symlink() {
68+
assert!(!file2.is_symlink());
69+
assert!(fs::read(file2)?.to_path()? == fs::read_link(file1)?);
70+
} else {
71+
assert!(fs::read(file1)? == fs::read(file2)?);
72+
#[cfg(unix)]
73+
assert!(fs::metadata(file1)?.mode() & 0b111 << 6 == fs::metadata(file2)?.mode() & 0b111 << 6);
74+
}
75+
}
76+
77+
Ok(())
78+
}
+19
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

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

0 commit comments

Comments
 (0)