1use 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
162pub 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}