1use std::env;
19use std::fs;
20use std::path::{Path, PathBuf};
21
22use crate::error::{Error, Result};
23use crate::odb::Odb;
24
25#[derive(Debug)]
27pub struct Repository {
28 pub git_dir: PathBuf,
30 pub work_tree: Option<PathBuf>,
32 pub odb: Odb,
34}
35
36impl Repository {
37 pub fn open(git_dir: &Path, work_tree: Option<&Path>) -> Result<Self> {
44 let git_dir = git_dir
45 .canonicalize()
46 .map_err(|_| Error::NotARepository(git_dir.display().to_string()))?;
47
48 if !git_dir.join("HEAD").exists() {
49 return Err(Error::NotARepository(git_dir.display().to_string()));
50 }
51
52 let objects_dir = if git_dir.join("objects").exists() {
55 git_dir.join("objects")
56 } else if let Ok(common_raw) = fs::read_to_string(git_dir.join("commondir")) {
57 let common_rel = common_raw.trim();
58 let common_dir = if Path::new(common_rel).is_absolute() {
59 PathBuf::from(common_rel)
60 } else {
61 git_dir.join(common_rel)
62 };
63 let common_dir = common_dir
64 .canonicalize()
65 .map_err(|_| Error::NotARepository(git_dir.display().to_string()))?;
66 common_dir.join("objects")
67 } else {
68 return Err(Error::NotARepository(git_dir.display().to_string()));
69 };
70
71 if !objects_dir.exists() {
72 return Err(Error::NotARepository(git_dir.display().to_string()));
73 }
74
75 let work_tree = match work_tree {
76 Some(p) => Some(
77 p.canonicalize()
78 .map_err(|_| Error::PathError(p.display().to_string()))?,
79 ),
80 None => None,
81 };
82
83 let odb = Odb::new(&objects_dir);
84
85 Ok(Self {
86 git_dir,
87 work_tree,
88 odb,
89 })
90 }
91
92 pub fn discover(start: Option<&Path>) -> Result<Self> {
101 if let Ok(dir) = env::var("GIT_DIR") {
103 let git_dir = PathBuf::from(dir);
104 let work_tree = env::var("GIT_WORK_TREE").ok().map(PathBuf::from);
105 return Self::open(&git_dir, work_tree.as_deref());
106 }
107
108 let cwd = env::current_dir()?;
109 let start = start.unwrap_or(&cwd);
110 let start = if start.is_absolute() {
111 start.to_path_buf()
112 } else {
113 cwd.join(start)
114 };
115
116 let ceiling_dirs = parse_ceiling_directories();
119
120 let mut current = start.as_path();
121 let mut first = true;
122 loop {
123 if !first && is_ceiling_blocked(current, &ceiling_dirs) {
127 break;
128 }
129 first = false;
130
131 if let Some(repo) = try_open_at(current)? {
132 return Ok(repo);
133 }
134 match current.parent() {
135 Some(p) => current = p,
136 None => break,
137 }
138 }
139
140 Err(Error::NotARepository(start.display().to_string()))
141 }
142
143 #[must_use]
145 pub fn index_path(&self) -> PathBuf {
146 self.git_dir.join("index")
147 }
148
149 #[must_use]
151 pub fn refs_dir(&self) -> PathBuf {
152 self.git_dir.join("refs")
153 }
154
155 #[must_use]
157 pub fn head_path(&self) -> PathBuf {
158 self.git_dir.join("HEAD")
159 }
160
161 #[must_use]
163 pub fn is_bare(&self) -> bool {
164 self.work_tree.is_none()
165 }
166}
167
168fn try_open_at(dir: &Path) -> Result<Option<Repository>> {
173 let dot_git = dir.join(".git");
174
175 if dot_git.is_file() {
176 let content =
178 fs::read_to_string(&dot_git).map_err(|e| Error::NotARepository(e.to_string()))?;
179 let git_dir = parse_gitfile(&content, dir)?;
180 let repo = Repository::open(&git_dir, Some(dir))?;
181 return Ok(Some(repo));
182 }
183
184 if dot_git.is_dir() {
185 let repo = Repository::open(&dot_git, Some(dir))?;
186 return Ok(Some(repo));
187 }
188
189 if dir.join("objects").is_dir() && dir.join("HEAD").is_file() {
191 let repo = Repository::open(dir, None)?;
192 return Ok(Some(repo));
193 }
194
195 Ok(None)
196}
197
198fn parse_gitfile(content: &str, base: &Path) -> Result<PathBuf> {
200 for line in content.lines() {
201 if let Some(rest) = line.strip_prefix("gitdir:") {
202 let rel = rest.trim();
203 let path = if Path::new(rel).is_absolute() {
204 PathBuf::from(rel)
205 } else {
206 base.join(rel)
207 };
208 return Ok(path);
209 }
210 }
211 Err(Error::NotARepository(
212 "gitfile does not contain 'gitdir:' line".to_owned(),
213 ))
214}
215
216pub fn init_repository(
233 path: &Path,
234 bare: bool,
235 initial_branch: &str,
236 template_dir: Option<&Path>,
237) -> Result<Repository> {
238 let git_dir = if bare {
239 path.to_path_buf()
240 } else {
241 path.join(".git")
242 };
243
244 for sub in &[
246 "objects",
247 "objects/info",
248 "objects/pack",
249 "refs",
250 "refs/heads",
251 "refs/tags",
252 "info",
253 "hooks",
254 ] {
255 fs::create_dir_all(git_dir.join(sub))?;
256 }
257
258 if let Some(tmpl) = template_dir {
260 if tmpl.is_dir() {
261 copy_template(tmpl, &git_dir)?;
262 }
263 }
264
265 let head_content = format!("ref: refs/heads/{initial_branch}\n");
267 fs::write(git_dir.join("HEAD"), head_content)?;
268
269 let config_content = if bare {
271 "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = true\n"
272 } else {
273 "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = false\n\tlogallrefupdates = true\n"
274 };
275 fs::write(git_dir.join("config"), config_content)?;
276
277 fs::write(
279 git_dir.join("description"),
280 "Unnamed repository; edit this file 'description' to name the repository.\n",
281 )?;
282
283 let work_tree = if bare { None } else { Some(path) };
284 Repository::open(&git_dir, work_tree)
285}
286
287fn copy_template(src: &Path, dst: &Path) -> Result<()> {
289 for entry in fs::read_dir(src)? {
290 let entry = entry?;
291 let src_path = entry.path();
292 let dst_path = dst.join(entry.file_name());
293 if src_path.is_dir() {
294 fs::create_dir_all(&dst_path)?;
295 copy_template(&src_path, &dst_path)?;
296 } else {
297 fs::copy(&src_path, &dst_path)?;
298 }
299 }
300 Ok(())
301}
302
303fn parse_ceiling_directories() -> Vec<PathBuf> {
308 let raw = match env::var("GIT_CEILING_DIRECTORIES") {
309 Ok(val) => val,
310 Err(_) => return Vec::new(),
311 };
312 if raw.is_empty() {
313 return Vec::new();
314 }
315 raw.split(':')
316 .filter(|s| !s.is_empty())
317 .filter_map(|s| {
318 let p = PathBuf::from(s);
319 if !p.is_absolute() {
320 return None;
321 }
322 Some(p.canonicalize().unwrap_or_else(|_| {
325 let s = s.trim_end_matches('/');
327 PathBuf::from(s)
328 }))
329 })
330 .collect()
331}
332
333fn is_ceiling_blocked(dir: &Path, ceilings: &[PathBuf]) -> bool {
343 if ceilings.is_empty() {
344 return false;
345 }
346 let canon = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
349 for ceil in ceilings {
350 if canon == *ceil {
355 return true;
356 }
357 }
358 false
359}