1use crate::repo;
2use anyhow::*;
3use git2::{ErrorCode, Repository, RepositoryInitOptions, Signature};
4use std::collections::HashSet;
5use std::ffi::{OsStr, OsString};
6use std::io::Write;
7use std::path::{self, PathBuf};
8use std::process::Command;
9
10const INITIAL_COMMIT_MESSAGE: &str = "Initial commit (git-wok assemble)";
11const DEFAULT_AUTHOR_NAME: &str = "git-wok assemble";
12const DEFAULT_AUTHOR_EMAIL: &str = "assemble@git-wok.dev";
13
14pub fn assemble<W: Write>(
15 workspace_dir: &path::Path,
16 config_path: &path::Path,
17 stdout: &mut W,
18) -> Result<()> {
19 if !workspace_dir.exists() {
20 bail!(
21 "Workspace directory `{}` does not exist",
22 workspace_dir.display()
23 );
24 }
25 if !workspace_dir.is_dir() {
26 bail!(
27 "Workspace path `{}` is not a directory",
28 workspace_dir.display()
29 );
30 }
31
32 writeln!(
33 stdout,
34 "Assembling workspace in `{}`",
35 workspace_dir.display()
36 )?;
37
38 let mut workspace_repo =
39 ensure_git_repo(workspace_dir, true).with_context(|| {
40 format!("Cannot prepare repo at `{}`", workspace_dir.display())
41 })?;
42 let mut submodule_paths = current_submodule_paths(&workspace_repo)?;
43
44 for entry in std::fs::read_dir(workspace_dir).with_context(|| {
45 format!(
46 "Cannot read workspace directory at `{}`",
47 workspace_dir.display()
48 )
49 })? {
50 let entry = entry?;
51 let file_type = entry.file_type()?;
52 if !file_type.is_dir() {
53 continue;
54 }
55
56 let name = entry.file_name();
57 if name == OsStr::new(".git") || name == OsStr::new(".gitmodules") {
58 continue;
59 }
60
61 let entry_path = entry.path();
62 let rel_path = entry_path.strip_prefix(workspace_dir).with_context(|| {
63 format!("Cannot derive relative path for `{}`", entry_path.display())
64 })?;
65
66 let rel_path = rel_path.to_path_buf();
67
68 let child_repo = ensure_git_repo(&entry_path, true).with_context(|| {
69 format!(
70 "Cannot prepare component repo at `{}`",
71 entry_path.display()
72 )
73 })?;
74
75 ensure_initial_commit(&child_repo)?;
76
77 if !submodule_paths.contains(&rel_path) {
78 let source_path = entry_path.canonicalize().with_context(|| {
79 format!("Cannot resolve path `{}`", entry_path.display())
80 })?;
81
82 register_submodule(workspace_dir, &rel_path, &source_path)?;
83
84 if let Some(remote_url) = repo_remote_url(&child_repo)? {
85 update_submodule_remote(workspace_dir, &rel_path, &remote_url)?;
86 }
87
88 writeln!(stdout, "Registered `{}` as submodule", rel_path.display())?;
89
90 workspace_repo = Repository::open(workspace_dir)?;
91 submodule_paths = current_submodule_paths(&workspace_repo)?;
92 }
93 }
94
95 let umbrella = repo::Repo::new(workspace_dir, None)?;
96 super::init::init(config_path, &umbrella, stdout)?;
97
98 Ok(())
99}
100
101fn ensure_git_repo(path: &path::Path, ensure_commit: bool) -> Result<Repository> {
102 let repo = match Repository::open(path) {
103 std::result::Result::Ok(repo) => repo,
104 Err(_) => {
105 let mut opts = RepositoryInitOptions::new();
106 opts.initial_head("main");
107 Repository::init_opts(path, &opts).with_context(|| {
108 format!("Cannot init git repo at `{}`", path.display())
109 })?
110 },
111 };
112
113 if ensure_commit {
114 ensure_initial_commit(&repo)?;
115 }
116
117 Ok(repo)
118}
119
120fn ensure_initial_commit(repo: &Repository) -> Result<()> {
121 match repo.head() {
122 std::result::Result::Ok(_) => Ok(()),
123 Err(err)
124 if err.code() == ErrorCode::UnbornBranch
125 || err.code() == ErrorCode::NotFound =>
126 {
127 let signature = Signature::now(DEFAULT_AUTHOR_NAME, DEFAULT_AUTHOR_EMAIL)
128 .context("Cannot create signature for initial commit")?;
129
130 let tree_id = {
131 let mut index = repo.index()?;
132 index.write_tree()?
133 };
134
135 let tree = repo.find_tree(tree_id)?;
136
137 repo.commit(
138 Some("HEAD"),
139 &signature,
140 &signature,
141 INITIAL_COMMIT_MESSAGE,
142 &tree,
143 &[],
144 )
145 .context("Cannot create initial commit")?;
146
147 Ok(())
148 },
149 Err(err) => Err(err.into()),
150 }
151}
152
153fn current_submodule_paths(repo: &Repository) -> Result<HashSet<PathBuf>> {
154 Ok(repo
155 .submodules()
156 .with_context(|| {
157 format!(
158 "Cannot list submodules for repo at `{}`",
159 repo.workdir()
160 .unwrap_or_else(|| path::Path::new("<unknown>"))
161 .display()
162 )
163 })?
164 .into_iter()
165 .map(|submodule| submodule.path().to_path_buf())
166 .collect())
167}
168
169fn register_submodule(
170 workspace_dir: &path::Path,
171 rel_path: &path::Path,
172 source_path: &path::Path,
173) -> Result<()> {
174 run_git(
175 workspace_dir,
176 [
177 OsStr::new("submodule"),
178 OsStr::new("add"),
179 source_path.as_os_str(),
180 rel_path.as_os_str(),
181 ],
182 )
183 .with_context(|| format!("Cannot add `{}` as submodule", rel_path.display()))?;
184
185 run_git(
186 workspace_dir,
187 [
188 OsStr::new("submodule"),
189 OsStr::new("absorbgitdirs"),
190 rel_path.as_os_str(),
191 ],
192 )
193 .with_context(|| {
194 format!(
195 "Cannot absorb git dir for submodule `{}`",
196 rel_path.display()
197 )
198 })?;
199
200 Ok(())
201}
202
203fn update_submodule_remote(
204 workspace_dir: &path::Path,
205 rel_path: &path::Path,
206 remote_url: &str,
207) -> Result<()> {
208 let key_os =
209 OsString::from(format!("submodule.{}.url", rel_path.to_string_lossy()));
210 let remote_os = OsString::from(remote_url);
211
212 run_git(
213 workspace_dir,
214 [
215 OsStr::new("config"),
216 OsStr::new("-f"),
217 OsStr::new(".gitmodules"),
218 key_os.as_os_str(),
219 remote_os.as_os_str(),
220 ],
221 )?;
222
223 run_git(
224 workspace_dir,
225 [
226 OsStr::new("config"),
227 key_os.as_os_str(),
228 remote_os.as_os_str(),
229 ],
230 )?;
231
232 Ok(())
233}
234
235fn repo_remote_url(repo: &Repository) -> Result<Option<String>> {
236 match repo.find_remote("origin") {
237 std::result::Result::Ok(remote) => Ok(remote.url().map(|url| url.to_string())),
238 Err(err) if err.code() == ErrorCode::NotFound => Ok(None),
239 Err(err) => Err(err.into()),
240 }
241}
242
243fn run_git<I, S>(cwd: &path::Path, args: I) -> Result<()>
244where
245 I: IntoIterator<Item = S>,
246 S: AsRef<OsStr>,
247{
248 let status = Command::new("git")
249 .args(args)
250 .current_dir(cwd)
251 .status()
252 .with_context(|| format!("Cannot execute git in `{}`", cwd.display()))?;
253
254 if !status.success() {
255 bail!("Git command failed in `{}`", cwd.display());
256 }
257
258 Ok(())
259}