Skip to main content

murk_cli/
git.rs

1//! Git integration helpers (merge driver setup).
2
3use std::fs;
4use std::io::Write;
5use std::path::Path;
6use std::process::Command;
7
8/// The `.gitattributes` line that enables the merge driver.
9const GITATTRIBUTES_LINE: &str = "*.murk merge=murk";
10
11/// Git config keys for the merge driver.
12const GIT_CONFIG_MERGE_NAME: &str = "merge.murk.name";
13const GIT_CONFIG_MERGE_DRIVER: &str = "merge.murk.driver";
14
15/// A step completed during merge driver setup.
16#[derive(Debug, PartialEq, Eq)]
17pub enum MergeDriverSetupStep {
18    /// `.gitattributes` already contained the merge driver entry.
19    GitattributesAlreadyExists,
20    /// Appended the merge driver entry to an existing `.gitattributes`.
21    GitattributesAppended,
22    /// Created a new `.gitattributes` file with the merge driver entry.
23    GitattributesCreated,
24    /// Configured `git config merge.murk.*`.
25    GitConfigured,
26}
27
28/// Configure git to use murk's custom merge driver for `.murk` files.
29///
30/// 1. Ensures `.gitattributes` contains `*.murk merge=murk`.
31/// 2. Runs `git config merge.murk.name` and `git config merge.murk.driver`.
32///
33/// Returns the steps that were performed.
34pub fn setup_merge_driver() -> Result<Vec<MergeDriverSetupStep>, String> {
35    let mut steps = Vec::new();
36
37    // 1. Write .gitattributes entry.
38    let gitattributes = Path::new(".gitattributes");
39    let merge_line = GITATTRIBUTES_LINE;
40
41    if gitattributes.exists() {
42        let contents = fs::read_to_string(gitattributes)
43            .map_err(|e| format!("reading .gitattributes: {e}"))?;
44        if contents.contains(merge_line) {
45            steps.push(MergeDriverSetupStep::GitattributesAlreadyExists);
46        } else {
47            let mut file = fs::OpenOptions::new()
48                .append(true)
49                .open(gitattributes)
50                .map_err(|e| format!("writing .gitattributes: {e}"))?;
51            writeln!(file, "{merge_line}").map_err(|e| format!("writing .gitattributes: {e}"))?;
52            steps.push(MergeDriverSetupStep::GitattributesAppended);
53        }
54    } else {
55        fs::write(gitattributes, format!("{merge_line}\n"))
56            .map_err(|e| format!("writing .gitattributes: {e}"))?;
57        steps.push(MergeDriverSetupStep::GitattributesCreated);
58    }
59
60    // 2. Configure git merge driver.
61    let configs = [
62        (GIT_CONFIG_MERGE_NAME, "murk vault merge"),
63        (GIT_CONFIG_MERGE_DRIVER, "murk merge-driver %O %A %B"),
64    ];
65    for (key, value) in &configs {
66        let status = Command::new("git")
67            .args(["config", key, value])
68            .status()
69            .map_err(|e| format!("running git config: {e}"))?;
70        if !status.success() {
71            return Err(format!("git config {key} failed (are you in a git repo?)"));
72        }
73    }
74    steps.push(MergeDriverSetupStep::GitConfigured);
75
76    Ok(steps)
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::testutil::CWD_LOCK;
83
84    #[test]
85    fn setup_merge_driver_creates_gitattributes() {
86        let _lock = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
87        let dir = std::env::temp_dir().join("murk_test_git_setup");
88        let _ = std::fs::remove_dir_all(&dir);
89        std::fs::create_dir_all(&dir).unwrap();
90
91        // Init a git repo so git config works.
92        Command::new("git")
93            .args(["init"])
94            .current_dir(&dir)
95            .output()
96            .unwrap();
97
98        let original_dir = std::env::current_dir().unwrap();
99        std::env::set_current_dir(&dir).unwrap();
100
101        let steps = setup_merge_driver().unwrap();
102        assert!(steps.contains(&MergeDriverSetupStep::GitattributesCreated));
103        assert!(steps.contains(&MergeDriverSetupStep::GitConfigured));
104
105        let contents = std::fs::read_to_string(dir.join(".gitattributes")).unwrap();
106        assert!(contents.contains("*.murk merge=murk"));
107
108        std::env::set_current_dir(original_dir).unwrap();
109        std::fs::remove_dir_all(&dir).unwrap();
110    }
111
112    #[test]
113    fn setup_merge_driver_appends_gitattributes() {
114        let _lock = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
115        let dir = std::env::temp_dir().join("murk_test_git_append");
116        let _ = std::fs::remove_dir_all(&dir);
117        std::fs::create_dir_all(&dir).unwrap();
118
119        Command::new("git")
120            .args(["init"])
121            .current_dir(&dir)
122            .output()
123            .unwrap();
124
125        std::fs::write(dir.join(".gitattributes"), "*.txt text\n").unwrap();
126
127        let original_dir = std::env::current_dir().unwrap();
128        std::env::set_current_dir(&dir).unwrap();
129
130        let steps = setup_merge_driver().unwrap();
131        assert!(steps.contains(&MergeDriverSetupStep::GitattributesAppended));
132
133        let contents = std::fs::read_to_string(dir.join(".gitattributes")).unwrap();
134        assert!(contents.contains("*.txt text"));
135        assert!(contents.contains("*.murk merge=murk"));
136
137        std::env::set_current_dir(original_dir).unwrap();
138        std::fs::remove_dir_all(&dir).unwrap();
139    }
140
141    #[test]
142    fn setup_merge_driver_already_exists() {
143        let _lock = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
144        let dir = std::env::temp_dir().join("murk_test_git_exists");
145        let _ = std::fs::remove_dir_all(&dir);
146        std::fs::create_dir_all(&dir).unwrap();
147
148        Command::new("git")
149            .args(["init"])
150            .current_dir(&dir)
151            .output()
152            .unwrap();
153
154        std::fs::write(dir.join(".gitattributes"), "*.murk merge=murk\n").unwrap();
155
156        let original_dir = std::env::current_dir().unwrap();
157        std::env::set_current_dir(&dir).unwrap();
158
159        let steps = setup_merge_driver().unwrap();
160        assert!(steps.contains(&MergeDriverSetupStep::GitattributesAlreadyExists));
161
162        std::env::set_current_dir(original_dir).unwrap();
163        std::fs::remove_dir_all(&dir).unwrap();
164    }
165}