1use std::fs;
4use std::io::Write;
5use std::path::Path;
6use std::process::Command;
7
8const GITATTRIBUTES_LINE: &str = "*.murk merge=murk";
10
11const GIT_CONFIG_MERGE_NAME: &str = "merge.murk.name";
13const GIT_CONFIG_MERGE_DRIVER: &str = "merge.murk.driver";
14
15#[derive(Debug, PartialEq, Eq)]
17pub enum MergeDriverSetupStep {
18 GitattributesAlreadyExists,
20 GitattributesAppended,
22 GitattributesCreated,
24 GitConfigured,
26}
27
28pub fn setup_merge_driver() -> Result<Vec<MergeDriverSetupStep>, String> {
35 let mut steps = Vec::new();
36
37 let gitattributes = Path::new(".gitattributes");
39 let merge_line = GITATTRIBUTES_LINE;
40
41 crate::env::reject_symlink(gitattributes, ".gitattributes")?;
42
43 if gitattributes.exists() {
44 let contents = fs::read_to_string(gitattributes)
45 .map_err(|e| format!("reading .gitattributes: {e}"))?;
46 if contents.contains(merge_line) {
47 steps.push(MergeDriverSetupStep::GitattributesAlreadyExists);
48 } else {
49 let mut file = fs::OpenOptions::new()
50 .append(true)
51 .open(gitattributes)
52 .map_err(|e| format!("writing .gitattributes: {e}"))?;
53 writeln!(file, "{merge_line}").map_err(|e| format!("writing .gitattributes: {e}"))?;
54 steps.push(MergeDriverSetupStep::GitattributesAppended);
55 }
56 } else {
57 fs::write(gitattributes, format!("{merge_line}\n"))
58 .map_err(|e| format!("writing .gitattributes: {e}"))?;
59 steps.push(MergeDriverSetupStep::GitattributesCreated);
60 }
61
62 let configs = [
64 (GIT_CONFIG_MERGE_NAME, "murk vault merge"),
65 (GIT_CONFIG_MERGE_DRIVER, "murk merge-driver %O %A %B"),
66 ];
67 for (key, value) in &configs {
68 let status = Command::new("git")
69 .args(["config", key, value])
70 .status()
71 .map_err(|e| format!("running git config: {e}"))?;
72 if !status.success() {
73 return Err(format!("git config {key} failed (are you in a git repo?)"));
74 }
75 }
76 steps.push(MergeDriverSetupStep::GitConfigured);
77
78 Ok(steps)
79}
80
81#[derive(Debug, PartialEq, Eq)]
88pub enum CommitSignature {
89 Good,
91 Unverified,
93 Bad,
95 Unsigned,
97}
98
99pub fn last_commit_signature(path: &str) -> Option<CommitSignature> {
103 let output = Command::new("git")
104 .args(["log", "-1", "--format=%G?", "--", path])
105 .output()
106 .ok()?;
107 if !output.status.success() {
108 return None;
109 }
110 match String::from_utf8(output.stdout).ok()?.trim() {
111 "G" => Some(CommitSignature::Good),
112 "U" | "X" | "Y" | "E" => Some(CommitSignature::Unverified),
115 "B" | "R" => Some(CommitSignature::Bad),
116 "N" => Some(CommitSignature::Unsigned),
117 _ => None,
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::testutil::CWD_LOCK;
126
127 #[test]
128 fn setup_merge_driver_creates_gitattributes() {
129 let _lock = CWD_LOCK
130 .lock()
131 .unwrap_or_else(std::sync::PoisonError::into_inner);
132 let dir = std::env::temp_dir().join("murk_test_git_setup");
133 let _ = std::fs::remove_dir_all(&dir);
134 std::fs::create_dir_all(&dir).unwrap();
135
136 Command::new("git")
138 .args(["init"])
139 .current_dir(&dir)
140 .output()
141 .unwrap();
142
143 let original_dir = std::env::current_dir().unwrap();
144 std::env::set_current_dir(&dir).unwrap();
145
146 let steps = setup_merge_driver().unwrap();
147 assert!(steps.contains(&MergeDriverSetupStep::GitattributesCreated));
148 assert!(steps.contains(&MergeDriverSetupStep::GitConfigured));
149
150 let contents = std::fs::read_to_string(dir.join(".gitattributes")).unwrap();
151 assert!(contents.contains("*.murk merge=murk"));
152
153 std::env::set_current_dir(original_dir).unwrap();
154 std::fs::remove_dir_all(&dir).unwrap();
155 }
156
157 #[test]
158 fn setup_merge_driver_appends_gitattributes() {
159 let _lock = CWD_LOCK
160 .lock()
161 .unwrap_or_else(std::sync::PoisonError::into_inner);
162 let dir = std::env::temp_dir().join("murk_test_git_append");
163 let _ = std::fs::remove_dir_all(&dir);
164 std::fs::create_dir_all(&dir).unwrap();
165
166 Command::new("git")
167 .args(["init"])
168 .current_dir(&dir)
169 .output()
170 .unwrap();
171
172 std::fs::write(dir.join(".gitattributes"), "*.txt text\n").unwrap();
173
174 let original_dir = std::env::current_dir().unwrap();
175 std::env::set_current_dir(&dir).unwrap();
176
177 let steps = setup_merge_driver().unwrap();
178 assert!(steps.contains(&MergeDriverSetupStep::GitattributesAppended));
179
180 let contents = std::fs::read_to_string(dir.join(".gitattributes")).unwrap();
181 assert!(contents.contains("*.txt text"));
182 assert!(contents.contains("*.murk merge=murk"));
183
184 std::env::set_current_dir(original_dir).unwrap();
185 std::fs::remove_dir_all(&dir).unwrap();
186 }
187
188 #[test]
189 fn setup_merge_driver_already_exists() {
190 let _lock = CWD_LOCK
191 .lock()
192 .unwrap_or_else(std::sync::PoisonError::into_inner);
193 let dir = std::env::temp_dir().join("murk_test_git_exists");
194 let _ = std::fs::remove_dir_all(&dir);
195 std::fs::create_dir_all(&dir).unwrap();
196
197 Command::new("git")
198 .args(["init"])
199 .current_dir(&dir)
200 .output()
201 .unwrap();
202
203 std::fs::write(dir.join(".gitattributes"), "*.murk merge=murk\n").unwrap();
204
205 let original_dir = std::env::current_dir().unwrap();
206 std::env::set_current_dir(&dir).unwrap();
207
208 let steps = setup_merge_driver().unwrap();
209 assert!(steps.contains(&MergeDriverSetupStep::GitattributesAlreadyExists));
210
211 std::env::set_current_dir(original_dir).unwrap();
212 std::fs::remove_dir_all(&dir).unwrap();
213 }
214}