]>
Commit | Line | Data |
---|---|---|
0a29b90c FG |
1 | use std::{ |
2 | convert::TryFrom, | |
3 | fs::{self, OpenOptions}, | |
4 | io::Write, | |
5 | path::{Path, PathBuf}, | |
6 | }; | |
7 | ||
8 | use gix_config::parse::section; | |
9 | use gix_discover::DOT_GIT_DIR; | |
781aab86 | 10 | use gix_macros::momo; |
0a29b90c FG |
11 | |
12 | /// The error used in [`into()`]. | |
13 | #[derive(Debug, thiserror::Error)] | |
14 | #[allow(missing_docs)] | |
15 | pub enum Error { | |
16 | #[error("Could not obtain the current directory")] | |
17 | CurrentDir(#[from] std::io::Error), | |
18 | #[error("Could not open data at '{}'", .path.display())] | |
19 | IoOpen { source: std::io::Error, path: PathBuf }, | |
20 | #[error("Could not write data at '{}'", .path.display())] | |
21 | IoWrite { source: std::io::Error, path: PathBuf }, | |
22 | #[error("Refusing to initialize the existing '{}' directory", .path.display())] | |
23 | DirectoryExists { path: PathBuf }, | |
24 | #[error("Refusing to initialize the non-empty directory as '{}'", .path.display())] | |
25 | DirectoryNotEmpty { path: PathBuf }, | |
26 | #[error("Could not create directory at '{}'", .path.display())] | |
27 | CreateDirectory { source: std::io::Error, path: PathBuf }, | |
28 | } | |
29 | ||
30 | /// The kind of repository to create. | |
31 | #[derive(Debug, Copy, Clone)] | |
32 | pub enum Kind { | |
33 | /// An empty repository with a `.git` folder, setup to contain files in its worktree. | |
34 | WithWorktree, | |
35 | /// A bare repository without a worktree. | |
36 | Bare, | |
37 | } | |
38 | ||
781aab86 FG |
39 | const TPL_INFO_EXCLUDE: &[u8] = include_bytes!("assets/init/info/exclude"); |
40 | const TPL_HOOKS_APPLYPATCH_MSG: &[u8] = include_bytes!("assets/init/hooks/applypatch-msg.sample"); | |
41 | const TPL_HOOKS_COMMIT_MSG: &[u8] = include_bytes!("assets/init/hooks/commit-msg.sample"); | |
42 | const TPL_HOOKS_FSMONITOR_WATCHMAN: &[u8] = include_bytes!("assets/init/hooks/fsmonitor-watchman.sample"); | |
43 | const TPL_HOOKS_POST_UPDATE: &[u8] = include_bytes!("assets/init/hooks/post-update.sample"); | |
44 | const TPL_HOOKS_PRE_APPLYPATCH: &[u8] = include_bytes!("assets/init/hooks/pre-applypatch.sample"); | |
45 | const TPL_HOOKS_PRE_COMMIT: &[u8] = include_bytes!("assets/init/hooks/pre-commit.sample"); | |
46 | const TPL_HOOKS_PRE_MERGE_COMMIT: &[u8] = include_bytes!("assets/init/hooks/pre-merge-commit.sample"); | |
47 | const TPL_HOOKS_PRE_PUSH: &[u8] = include_bytes!("assets/init/hooks/pre-push.sample"); | |
48 | const TPL_HOOKS_PRE_REBASE: &[u8] = include_bytes!("assets/init/hooks/pre-rebase.sample"); | |
49 | const TPL_HOOKS_PREPARE_COMMIT_MSG: &[u8] = include_bytes!("assets/init/hooks/prepare-commit-msg.sample"); | |
50 | const TPL_HOOKS_DOCS_URL: &[u8] = include_bytes!("assets/init/hooks/docs.url"); | |
51 | const TPL_DESCRIPTION: &[u8] = include_bytes!("assets/init/description"); | |
52 | const TPL_HEAD: &[u8] = include_bytes!("assets/init/HEAD"); | |
0a29b90c FG |
53 | |
54 | struct PathCursor<'a>(&'a mut PathBuf); | |
55 | ||
56 | struct NewDir<'a>(&'a mut PathBuf); | |
57 | ||
58 | impl<'a> PathCursor<'a> { | |
59 | fn at(&mut self, component: &str) -> &Path { | |
60 | self.0.push(component); | |
61 | self.0.as_path() | |
62 | } | |
63 | } | |
64 | ||
65 | impl<'a> NewDir<'a> { | |
66 | fn at(self, component: &str) -> Result<Self, Error> { | |
67 | self.0.push(component); | |
68 | create_dir(self.0)?; | |
69 | Ok(self) | |
70 | } | |
71 | fn as_mut(&mut self) -> &mut PathBuf { | |
72 | self.0 | |
73 | } | |
74 | } | |
75 | ||
76 | impl<'a> Drop for NewDir<'a> { | |
77 | fn drop(&mut self) { | |
78 | self.0.pop(); | |
79 | } | |
80 | } | |
81 | ||
82 | impl<'a> Drop for PathCursor<'a> { | |
83 | fn drop(&mut self) { | |
84 | self.0.pop(); | |
85 | } | |
86 | } | |
87 | ||
88 | fn write_file(data: &[u8], path: &Path) -> Result<(), Error> { | |
89 | let mut file = OpenOptions::new() | |
90 | .write(true) | |
91 | .create(true) | |
92 | .append(false) | |
93 | .open(path) | |
94 | .map_err(|e| Error::IoOpen { | |
95 | source: e, | |
96 | path: path.to_owned(), | |
97 | })?; | |
98 | file.write_all(data).map_err(|e| Error::IoWrite { | |
99 | source: e, | |
100 | path: path.to_owned(), | |
101 | }) | |
102 | } | |
103 | ||
104 | fn create_dir(p: &Path) -> Result<(), Error> { | |
105 | fs::create_dir_all(p).map_err(|e| Error::CreateDirectory { | |
106 | source: e, | |
107 | path: p.to_owned(), | |
108 | }) | |
109 | } | |
110 | ||
111 | /// Options for use in [`into()`]; | |
112 | #[derive(Copy, Clone, Default)] | |
113 | pub struct Options { | |
114 | /// If true, and the kind of repository to create has a worktree, then the destination directory must be empty. | |
115 | /// | |
116 | /// By default repos with worktree can be initialized into a non-empty repository as long as there is no `.git` directory. | |
117 | pub destination_must_be_empty: bool, | |
781aab86 | 118 | /// If set, use these filesystem capabilities to populate the respective git-config fields. |
0a29b90c | 119 | /// If `None`, the directory will be probed. |
49aad941 | 120 | pub fs_capabilities: Option<gix_fs::Capabilities>, |
0a29b90c FG |
121 | } |
122 | ||
123 | /// Create a new `.git` repository of `kind` within the possibly non-existing `directory` | |
124 | /// and return its path. | |
125 | /// Note that this is a simple template-based initialization routine which should be accompanied with additional corrections | |
126 | /// to respect git configuration, which is accomplished by [its callers][crate::ThreadSafeRepository::init_opts()] | |
127 | /// that return a [Repository][crate::Repository]. | |
781aab86 | 128 | #[momo] |
0a29b90c FG |
129 | pub fn into( |
130 | directory: impl Into<PathBuf>, | |
131 | kind: Kind, | |
132 | Options { | |
133 | fs_capabilities, | |
134 | destination_must_be_empty, | |
135 | }: Options, | |
136 | ) -> Result<gix_discover::repository::Path, Error> { | |
137 | let mut dot_git = directory.into(); | |
138 | let bare = matches!(kind, Kind::Bare); | |
139 | ||
140 | if bare || destination_must_be_empty { | |
141 | let num_entries_in_dot_git = fs::read_dir(&dot_git) | |
142 | .or_else(|err| { | |
143 | if err.kind() == std::io::ErrorKind::NotFound { | |
144 | fs::create_dir(&dot_git).and_then(|_| fs::read_dir(&dot_git)) | |
145 | } else { | |
146 | Err(err) | |
147 | } | |
148 | }) | |
149 | .map_err(|err| Error::IoOpen { | |
150 | source: err, | |
151 | path: dot_git.clone(), | |
152 | })? | |
153 | .count(); | |
154 | if num_entries_in_dot_git != 0 { | |
155 | return Err(Error::DirectoryNotEmpty { path: dot_git }); | |
156 | } | |
157 | } | |
158 | ||
159 | if !bare { | |
160 | dot_git.push(DOT_GIT_DIR); | |
161 | ||
162 | if dot_git.is_dir() { | |
163 | return Err(Error::DirectoryExists { path: dot_git }); | |
164 | } | |
165 | }; | |
166 | create_dir(&dot_git)?; | |
167 | ||
168 | { | |
169 | let mut cursor = NewDir(&mut dot_git).at("info")?; | |
170 | write_file(TPL_INFO_EXCLUDE, PathCursor(cursor.as_mut()).at("exclude"))?; | |
171 | } | |
172 | ||
173 | { | |
174 | let mut cursor = NewDir(&mut dot_git).at("hooks")?; | |
175 | for (tpl, filename) in &[ | |
781aab86 | 176 | (TPL_HOOKS_DOCS_URL, "docs.url"), |
0a29b90c | 177 | (TPL_HOOKS_PREPARE_COMMIT_MSG, "prepare-commit-msg.sample"), |
0a29b90c FG |
178 | (TPL_HOOKS_PRE_REBASE, "pre-rebase.sample"), |
179 | (TPL_HOOKS_PRE_PUSH, "pre-push.sample"), | |
180 | (TPL_HOOKS_PRE_COMMIT, "pre-commit.sample"), | |
181 | (TPL_HOOKS_PRE_MERGE_COMMIT, "pre-merge-commit.sample"), | |
182 | (TPL_HOOKS_PRE_APPLYPATCH, "pre-applypatch.sample"), | |
183 | (TPL_HOOKS_POST_UPDATE, "post-update.sample"), | |
184 | (TPL_HOOKS_FSMONITOR_WATCHMAN, "fsmonitor-watchman.sample"), | |
185 | (TPL_HOOKS_COMMIT_MSG, "commit-msg.sample"), | |
186 | (TPL_HOOKS_APPLYPATCH_MSG, "applypatch-msg.sample"), | |
187 | ] { | |
188 | write_file(tpl, PathCursor(cursor.as_mut()).at(filename))?; | |
189 | } | |
190 | } | |
191 | ||
192 | { | |
193 | let mut cursor = NewDir(&mut dot_git).at("objects")?; | |
194 | create_dir(PathCursor(cursor.as_mut()).at("info"))?; | |
195 | create_dir(PathCursor(cursor.as_mut()).at("pack"))?; | |
196 | } | |
197 | ||
198 | { | |
199 | let mut cursor = NewDir(&mut dot_git).at("refs")?; | |
200 | create_dir(PathCursor(cursor.as_mut()).at("heads"))?; | |
201 | create_dir(PathCursor(cursor.as_mut()).at("tags"))?; | |
202 | } | |
203 | ||
204 | for (tpl, filename) in &[(TPL_HEAD, "HEAD"), (TPL_DESCRIPTION, "description")] { | |
205 | write_file(tpl, PathCursor(&mut dot_git).at(filename))?; | |
206 | } | |
207 | ||
208 | { | |
209 | let mut config = gix_config::File::default(); | |
210 | { | |
49aad941 | 211 | let caps = fs_capabilities.unwrap_or_else(|| gix_fs::Capabilities::probe(&dot_git)); |
0a29b90c FG |
212 | let mut core = config.new_section("core", None).expect("valid section name"); |
213 | ||
214 | core.push(key("repositoryformatversion"), Some("0".into())); | |
215 | core.push(key("filemode"), Some(bool(caps.executable_bit).into())); | |
216 | core.push(key("bare"), Some(bool(bare).into())); | |
217 | core.push(key("logallrefupdates"), Some(bool(!bare).into())); | |
218 | core.push(key("symlinks"), Some(bool(caps.symlink).into())); | |
219 | core.push(key("ignorecase"), Some(bool(caps.ignore_case).into())); | |
220 | core.push(key("precomposeunicode"), Some(bool(caps.precompose_unicode).into())); | |
221 | } | |
222 | let mut cursor = PathCursor(&mut dot_git); | |
223 | let config_path = cursor.at("config"); | |
224 | std::fs::write(config_path, config.to_bstring()).map_err(|err| Error::IoWrite { | |
225 | source: err, | |
226 | path: config_path.to_owned(), | |
227 | })?; | |
228 | } | |
229 | ||
230 | Ok(gix_discover::repository::Path::from_dot_git_dir( | |
231 | dot_git, | |
232 | if bare { | |
4b012472 | 233 | gix_discover::repository::Kind::PossiblyBare |
0a29b90c FG |
234 | } else { |
235 | gix_discover::repository::Kind::WorkTree { linked_git_dir: None } | |
236 | }, | |
781aab86 | 237 | &std::env::current_dir()?, |
0a29b90c FG |
238 | ) |
239 | .expect("by now the `dot_git` dir is valid as we have accessed it")) | |
240 | } | |
241 | ||
242 | fn key(name: &'static str) -> section::Key<'static> { | |
243 | section::Key::try_from(name).expect("valid key name") | |
244 | } | |
245 | ||
246 | fn bool(v: bool) -> &'static str { | |
247 | match v { | |
248 | true => "true", | |
249 | false => "false", | |
250 | } | |
251 | } |