Skip to main content

tagit_sub_impl/
lib.rs

1//! Implementation of `tagit sub` command.
2
3use std::{
4    collections::BTreeSet,
5    path::{Path, PathBuf},
6    process::Command,
7};
8
9use owo_colors::OwoColorize;
10use tagit_core::{Okie, Tagit};
11use tagit_sub_command::SubtreeCommand;
12use tagit_sub_core::{TAGIT_ATTRIBUTES_FILE, TAGIT_SUB_DIR, path_to_remote, remote_to_path};
13
14const GITATTRIBUTES: &str = r#"/** linguist-vendored
15"#;
16
17fn save_attributes() -> anyhow::Result<()> {
18    std::fs::write(TAGIT_ATTRIBUTES_FILE, GITATTRIBUTES)?;
19    Ok(())
20}
21
22fn full_path(path: impl AsRef<Path>) -> PathBuf {
23    Path::new(TAGIT_SUB_DIR).join(path)
24}
25
26fn read_from(name: &str) -> anyhow::Result<String> {
27    let branch = Tagit::current_branch()?;
28    Ok(format!("{name}/{branch}"))
29}
30
31fn subs() -> anyhow::Result<Vec<PathBuf>> {
32    let mut subs = Vec::new();
33    let remotes = remotes()?;
34    for remote in remotes.iter() {
35        if let Some(path) = remote_to_path(remote).transpose() {
36            match path {
37                Ok(path) => {
38                    subs.push(path.into());
39                }
40                Err(path) => {
41                    eprintln!("missing: {}", path.path().red());
42                }
43            }
44        }
45    }
46    Ok(subs)
47}
48
49fn remotes() -> anyhow::Result<BTreeSet<String>> {
50    let output = Command::new("git").arg("remote").output()?;
51    output.status.okie()?;
52    Ok(String::from_utf8(output.stdout)?
53        .lines()
54        .map(|s| s.trim())
55        .filter(|s| !s.is_empty())
56        .map(|s| s.to_owned())
57        .collect())
58}
59
60fn add(path: PathBuf, remote: String) -> anyhow::Result<()> {
61    let name = path_to_remote(&path)?;
62    let read_from = read_from(&name)?;
63    let path = full_path(&path);
64    let remotes = remotes()?;
65    if !remotes.iter().any(|remote| *remote == name) {
66        Command::new("git")
67            .arg("remote")
68            .arg("add")
69            .arg("--fetch")
70            .arg("--no-tags")
71            .arg(&name)
72            .arg(remote)
73            .status()?
74            .okie()?;
75    }
76    Command::new("git")
77        .arg("merge")
78        .args(["-s", "ours"])
79        .arg("--no-commit")
80        .arg("-m")
81        .arg(format!("add {}", path.display()))
82        .arg("--allow-unrelated-histories")
83        .arg(&read_from)
84        .status()?
85        .okie()?;
86    let root = Tagit::root()?;
87    let path = path
88        .to_str()
89        .ok_or("non-utf8 path")
90        .map_err(anyhow::Error::msg)?
91        .replace('\\', "/");
92    if !root.join(&path).exists() {
93        Command::new("git")
94            .arg("read-tree")
95            .arg("--prefix")
96            .arg(&path)
97            .args(["-u", &read_from])
98            .status()?
99            .okie()?;
100    }
101    Ok(())
102}
103
104fn diff(path: PathBuf) -> anyhow::Result<()> {
105    let name = path_to_remote(&path)?;
106    let read_from = read_from(&name)?;
107    let path = full_path(&path);
108    Command::new("git")
109        .arg("diff")
110        .arg(format!("HEAD:{}", path.display()))
111        .arg(read_from)
112        .status()?
113        .okie()?;
114    Ok(())
115}
116
117fn pull(path: PathBuf) -> anyhow::Result<()> {
118    if path.as_os_str().to_str() == Some("*") {
119        for path in subs()? {
120            assert_ne!(path.as_os_str().to_str(), Some("*"));
121            pull(path)?;
122        }
123    } else {
124        let name = path_to_remote(&path)?;
125        let read_from = read_from(&name)?;
126        Command::new("git")
127            .arg("fetch")
128            .arg(&name)
129            .status()?
130            .okie()?;
131        Command::new("git")
132            .arg("merge")
133            .args(["-s", "subtree"])
134            .arg("--no-commit")
135            .arg("-m")
136            .arg(format!("update {}", path.display()))
137            .arg(&read_from)
138            .status()?
139            .okie()?;
140    }
141    Ok(())
142}
143
144fn remove(path: PathBuf) -> anyhow::Result<()> {
145    let name = path_to_remote(&path)?;
146    Command::new("git")
147        .arg("remote")
148        .arg("remove")
149        .arg(&name)
150        .status()?
151        .okie()?;
152    Ok(())
153}
154
155fn ls() -> anyhow::Result<()> {
156    for sub in subs()? {
157        println!("{}", sub.display());
158    }
159    Ok(())
160}
161
162/// Handle subcommands for `tagit sub`.
163pub fn sub(command: SubtreeCommand) -> anyhow::Result<()> {
164    match command {
165        SubtreeCommand::Add { path, remote } => {
166            add(path, remote)?;
167            save_attributes()
168        }
169        SubtreeCommand::Diff { path } => diff(path),
170        SubtreeCommand::Pull { path } => {
171            pull(path)?;
172            save_attributes()
173        }
174        SubtreeCommand::Remove { path } => {
175            remove(path)?;
176            save_attributes()
177        }
178        SubtreeCommand::Ls => ls(),
179    }
180}