1use std::fs;
12use std::io::{self, Read, Write};
13use std::path::PathBuf;
14use std::process::Command;
15
16pub const MAX_COMMIT_MSG_BYTES: u64 = 1024 * 1024;
18
19pub fn spawn_editor(template: &str) -> io::Result<String> {
40 let editor = pick_editor();
41 if editor.is_empty() {
42 return Err(io::Error::other(
43 "no editor configured; set $EDITOR or pass -m <msg>",
44 ));
45 }
46
47 let tmp = tempfile::NamedTempFile::with_suffix(".mkit-commit.txt")?;
51 {
52 let mut f = fs::OpenOptions::new().write(true).open(tmp.path())?;
53 f.write_all(template.as_bytes())?;
54 f.sync_all()?;
55 }
56
57 let mut parts = editor.split_whitespace();
63 let program = parts.next().unwrap_or("");
64 let extra_args: Vec<&str> = parts.collect();
65 let path_arg: PathBuf = tmp.path().to_path_buf();
66
67 let status = Command::new(program)
68 .args(&extra_args)
69 .arg(&path_arg)
70 .status()?;
71 if !status.success() {
72 return Err(io::Error::other(format!(
73 "editor exited with status {status:?}"
74 )));
75 }
76
77 let f = fs::File::open(tmp.path())?;
79 let mut buf = Vec::new();
80 f.take(MAX_COMMIT_MSG_BYTES + 1)
81 .read_to_end(&mut buf)
82 .map_err(io::Error::other)?;
83 if buf.len() as u64 > MAX_COMMIT_MSG_BYTES {
84 return Err(io::Error::other("commit message file too large (>1 MiB)"));
85 }
86 let raw = String::from_utf8_lossy(&buf).into_owned();
87 Ok(strip_comments_and_trim(&raw))
88}
89
90fn pick_editor() -> String {
93 pick_editor_with(|name| std::env::var(name).ok())
94}
95
96fn pick_editor_with(resolver: impl Fn(&str) -> Option<String>) -> String {
101 for var in ["GIT_EDITOR", "EDITOR", "VISUAL"] {
102 if let Some(v) = resolver(var)
103 && !v.trim().is_empty()
104 {
105 return v;
106 }
107 }
108 if cfg!(windows) {
109 "notepad".to_string()
110 } else {
111 "vi".to_string()
112 }
113}
114
115#[must_use]
118pub fn strip_comments_and_trim(input: &str) -> String {
119 let mut out = String::with_capacity(input.len());
120 for line in input.split('\n') {
121 let first_nws = line.trim_start();
122 if first_nws.starts_with('#') {
123 continue;
124 }
125 out.push_str(line);
126 out.push('\n');
127 }
128 out.trim_matches(|c: char| c == ' ' || c == '\t' || c == '\r' || c == '\n')
129 .to_string()
130}
131
132pub const COMMIT_EDITMSG_TEMPLATE: &str = "\n\
134# Please enter the commit message for your changes. Lines starting\n\
135# with '#' will be ignored, and an empty message aborts the commit.\n";
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn strip_comments_drops_hash_lines() {
143 let input = "\nhello\n# a comment\nworld\n # indented comment\n\n";
144 let out = strip_comments_and_trim(input);
145 assert_eq!(out, "hello\nworld");
146 }
147
148 #[test]
149 fn strip_comments_all_comment_yields_empty() {
150 let out = strip_comments_and_trim("# foo\n# bar\n");
151 assert!(out.is_empty());
152 }
153
154 #[test]
155 fn strip_comments_trims_trailing_crlf() {
156 let out = strip_comments_and_trim("hello\r\n# drop\r\n\r\n");
157 assert_eq!(out, "hello");
158 }
159
160 #[test]
161 fn pick_editor_prefers_git_editor_over_editor() {
162 let got = pick_editor_with(|name| match name {
163 "GIT_EDITOR" => Some("from-git".to_string()),
164 "EDITOR" => Some("from-editor".to_string()),
165 _ => None,
166 });
167 assert_eq!(got, "from-git");
168 }
169
170 #[test]
171 fn pick_editor_prefers_editor_over_visual() {
172 let got = pick_editor_with(|name| match name {
173 "EDITOR" => Some("from-editor".to_string()),
174 "VISUAL" => Some("from-visual".to_string()),
175 _ => None,
176 });
177 assert_eq!(got, "from-editor");
178 }
179
180 #[test]
181 fn pick_editor_skips_empty_strings() {
182 let got = pick_editor_with(|name| match name {
183 "GIT_EDITOR" => Some(String::new()),
184 "EDITOR" => Some(" ".to_string()),
185 "VISUAL" => Some("nano".to_string()),
186 _ => None,
187 });
188 assert_eq!(got, "nano");
189 }
190
191 #[test]
192 fn pick_editor_falls_back_to_platform_default() {
193 let got = pick_editor_with(|_| None);
194 if cfg!(windows) {
195 assert_eq!(got, "notepad");
196 } else {
197 assert_eq!(got, "vi");
198 }
199 }
200}