1use std::path::{Component, Path};
15
16use kernel::GitOid;
17use kernel::event::record::FileRestored;
18
19use crate::alias::WriteTarget;
20use crate::bundle::landing::{Bits, land};
21use crate::error::StorageError;
22use crate::real_fs::RealFs;
23
24use super::lease::WorktreeLease;
25use super::name::WorktreeName;
26use super::trees::Worktrees;
27
28impl Worktrees {
29 pub fn claim_at(
38 &self,
39 name: &WorktreeName,
40 point: &GitOid,
41 ) -> Result<WorktreeLease, StorageError> {
42 let busy = |detail: &str| StorageError::WorktreeBusy {
43 name: name.as_str().to_owned(),
44 detail: detail.to_owned(),
45 };
46 if self.live()?.contains(name) {
47 return Err(busy("a live tree holds this name"));
48 }
49 let city = self.refuse_oversized(name.as_str())?;
50 let commit = self.commit_at(point)?;
51 let branch = self
54 .repo
55 .branch(name.as_str(), &commit, false)
56 .map_err(|err| {
57 if err.code() == git2::ErrorCode::Exists {
58 busy("a line of work already has this name")
59 } else {
60 StorageError::Worktree {
61 op: "branch at the point to go back to",
62 detail: format!("{point}: {err}"),
63 }
64 }
65 })?;
66 match self.add_tree(name, Some(branch.get()), &city) {
67 Ok(lease) => Ok(lease),
68 Err(refusal) => {
69 let mut branch = branch;
72 branch.delete().map_err(|err| StorageError::Worktree {
73 op: "remove the branch of a tree that did not open",
74 detail: format!("{}: {err}; the tree failed with: {refusal}", name.as_str()),
75 })?;
76 Err(refusal)
77 }
78 }
79 }
80
81 pub fn restore_file(
91 &self,
92 lease: &WorktreeLease,
93 point: &GitOid,
94 path: &Path,
95 ) -> Result<FileRestored, StorageError> {
96 let refuse = |detail: String| StorageError::Worktree {
97 op: "restore a file from a point",
98 detail,
99 };
100 let inside = in_tree(path).ok_or_else(|| {
101 refuse(format!(
102 "{} is not a plain path inside the tree",
103 path.display()
104 ))
105 })?;
106 let tree = self
107 .commit_at(point)?
108 .tree()
109 .map_err(|err| refuse(format!("{point}: {err}")))?;
110 let target = WriteTarget::within(
111 "restore a file from a point",
112 lease.path(),
113 &lease.path().join(path),
114 )?;
115 let io = |source: std::io::Error| StorageError::Io {
116 op: "restore a file from a point",
117 path: target.as_path().to_path_buf(),
118 source,
119 };
120 let restored = FileRestored {
121 name: lease.name().as_str().to_owned(),
122 path: inside,
123 point: *point,
124 };
125 let entry = match tree.get_path(Path::new(&restored.path)) {
126 Ok(entry) => entry,
127 Err(err) if err.code() == git2::ErrorCode::NotFound => {
128 return match std::fs::remove_file(target.as_path()) {
129 Ok(()) => Ok(restored),
130 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(restored),
131 Err(err) => Err(io(err)),
132 };
133 }
134 Err(err) => return Err(refuse(format!("{}: {err}", path.display()))),
135 };
136 let object = entry
137 .to_object(&self.repo)
138 .map_err(|err| refuse(format!("{}: {err}", path.display())))?;
139 let blob = object
140 .as_blob()
141 .ok_or_else(|| refuse(format!("{} is not a file at {point}", path.display())))?;
142 if let Some(parent) = target.as_path().parent() {
143 std::fs::create_dir_all(parent).map_err(io)?;
144 }
145 land(&mut RealFs::new(), target, blob.content(), Bits::OfReplaced)?;
146 Ok(restored)
147 }
148
149 fn commit_at(&self, point: &GitOid) -> Result<git2::Commit<'_>, StorageError> {
150 let missing = |err: git2::Error| StorageError::Worktree {
151 op: "find the point to go back to",
152 detail: format!("{point}: {err}"),
153 };
154 let oid = git2::Oid::from_str(&point.to_string()).map_err(missing)?;
155 self.repo.find_commit(oid).map_err(missing)
156 }
157}
158
159fn in_tree(path: &Path) -> Option<String> {
166 let segments = path
167 .components()
168 .map(|component| match component {
169 Component::Normal(segment) => segment.to_str().filter(|segment| {
170 !segment.ends_with(['.', ' '])
171 && !segment.contains(':')
172 && !segment
173 .split('~')
174 .skip(1)
175 .any(|after| after.starts_with(|c: char| c.is_ascii_digit()))
176 }),
177 Component::Prefix(_)
178 | Component::RootDir
179 | Component::CurDir
180 | Component::ParentDir => None,
181 })
182 .collect::<Option<Vec<_>>>()?;
183 (!segments.is_empty() && crate::reserved::outside_reserved(path)).then(|| segments.join("/"))
184}
185
186#[cfg(test)]
187#[allow(
188 clippy::unwrap_used,
189 clippy::expect_used,
190 clippy::panic,
191 clippy::indexing_slicing,
192 reason = "test code"
193)]
194mod tests;