Skip to main content

submodule_kit/
submodule.rs

1use crate::strings;
2use std::path::Path;
3
4#[derive(Debug)]
5pub struct SubmoduleInfo {
6    pub path: String,
7    pub url: String,
8    pub branch: Option<String>,
9}
10
11pub fn parse_gitmodules_str(content: &str) -> Result<Vec<SubmoduleInfo>, String> {
12    let mut submodules: Vec<SubmoduleInfo> = Vec::new();
13    let mut current_name: Option<String> = None;
14    let mut current_path: Option<String> = None;
15    let mut current_url: Option<String> = None;
16    let mut current_branch: Option<String> = None;
17
18    let flush = |name: String,
19                 path: Option<String>,
20                 url: Option<String>,
21                 branch: Option<String>,
22                 out: &mut Vec<SubmoduleInfo>|
23     -> Result<(), String> {
24        let path = path.ok_or_else(|| strings::err_missing_path(&name))?;
25        let url = url.ok_or_else(|| strings::err_missing_url(&name))?;
26        out.push(SubmoduleInfo { path, url, branch });
27        Ok(())
28    };
29
30    for line in content.lines() {
31        let line = line.trim();
32        if line.starts_with(strings::SUBMODULE_SECTION_CHECK) && line.ends_with(']') {
33            if let Some(name) = current_name.take() {
34                flush(
35                    name,
36                    current_path.take(),
37                    current_url.take(),
38                    current_branch.take(),
39                    &mut submodules,
40                )?;
41            }
42            current_name = Some(
43                line.trim_start_matches(strings::SUBMODULE_SECTION_PREFIX)
44                    .trim_end_matches(strings::SUBMODULE_SECTION_SUFFIX)
45                    .to_string(),
46            );
47        } else if let Some(v) = line.strip_prefix(strings::KEY_PATH) {
48            current_path = Some(v.trim().to_string());
49        } else if let Some(v) = line.strip_prefix(strings::KEY_URL) {
50            current_url = Some(v.trim().to_string());
51        } else if let Some(v) = line.strip_prefix(strings::KEY_BRANCH) {
52            current_branch = Some(v.trim().to_string());
53        }
54    }
55
56    if let Some(name) = current_name.take() {
57        flush(
58            name,
59            current_path.take(),
60            current_url.take(),
61            current_branch.take(),
62            &mut submodules,
63        )?;
64    }
65
66    Ok(submodules)
67}
68
69pub fn parse_gitmodules() -> Result<Vec<SubmoduleInfo>, String> {
70    let content = std::fs::read_to_string(strings::GITMODULES_FILE)
71        .map_err(|e| strings::err_read_gitmodules(&e))?;
72    parse_gitmodules_str(&content)
73}
74
75pub fn git_rev_parse_submodule(repo: &git2::Repository, path: &str) -> Result<String, String> {
76    let index = repo.index().map_err(|e| strings::err_open_index(&e))?;
77    let entry = index
78        .get_path(Path::new(path), 0)
79        .ok_or_else(|| strings::err_not_in_index(path))?;
80    Ok(entry.id.to_string())
81}
82
83pub fn git_ls_remote(repo: &git2::Repository, url: &str, branch: &str) -> Result<String, String> {
84    let refspec = format!("{}{branch}", strings::REFS_HEADS_PREFIX);
85    let mut remote = repo
86        .remote_anonymous(url)
87        .map_err(|e| strings::err_create_remote(url, &e))?;
88    let mut callbacks = git2::RemoteCallbacks::new();
89    callbacks.credentials(|_url, username, _allowed| {
90        git2::Cred::ssh_key_from_agent(username.unwrap_or("git"))
91    });
92    remote
93        .connect_auth(git2::Direction::Fetch, Some(callbacks), None)
94        .map_err(|e| strings::err_connect_remote(url, &e))?;
95    let list = remote.list().map_err(|e| strings::err_list_refs(url, &e))?;
96    list.iter()
97        .find(|r| r.name() == refspec)
98        .map(|r| r.oid().to_string())
99        .ok_or_else(|| strings::err_ref_not_found(&refspec, url))
100}
101
102pub fn short(sha: &str) -> &str {
103    &sha[..sha.len().min(7)]
104}
105
106#[cfg(test)]
107#[path = "submodule_tests.rs"]
108mod tests;