Skip to main content

git_wok/cmd/
assemble.rs

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}