1use std::io::Write;
14
15use clap::{Parser, ValueEnum};
16use mkit_core::object::Object;
17use mkit_core::refs::{self, Head};
18use mkit_core::store::ObjectStore;
19
20use crate::clap_shim;
21use crate::exit;
22use crate::format;
23
24const DEFAULT_ABBREV: usize = 7;
27
28#[derive(Debug, Clone, Copy, ValueEnum)]
29enum BranchFormat {
30 Default,
31 Json,
32}
33
34#[derive(Debug, Parser)]
35#[command(
36 name = "mkit branch",
37 about = "List, create, rename, or delete branches."
38)]
39#[allow(clippy::struct_excessive_bools)] struct BranchOpts {
41 #[arg(short = 'd', long)]
44 delete: bool,
45 #[arg(short = 'D')]
50 force_delete: bool,
51 #[arg(short = 'm', long)]
55 rename: bool,
56 #[arg(short = 'v', long)]
59 verbose: bool,
60 #[arg(long, value_enum, default_value = "default")]
62 format: BranchFormat,
63 #[arg(num_args = 0..=2)]
65 names: Vec<String>,
66}
67
68#[must_use]
69pub fn run(args: &[String]) -> u8 {
70 let opts = match clap_shim::parse::<BranchOpts>("mkit branch", args) {
71 Ok(o) => o,
72 Err(code) => return code,
73 };
74 let cwd = match std::env::current_dir() {
75 Ok(p) => p,
76 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
77 };
78 let mkit_dir = cwd.join(mkit_core::MKIT_DIR);
79
80 let mode_flags = u8::from(opts.delete) + u8::from(opts.force_delete) + u8::from(opts.rename);
82 if mode_flags > 1 {
83 return super::usage_error("usage: mkit branch [-d|-D|-m] ... (modes are exclusive)");
84 }
85
86 if opts.rename {
87 return rename(&mkit_dir, &opts.names);
88 }
89 if opts.delete || opts.force_delete {
90 return delete(&mkit_dir, &opts.names, opts.force_delete);
91 }
92
93 match opts.names.as_slice() {
94 [] => list(
95 &cwd,
96 &mkit_dir,
97 matches!(opts.format, BranchFormat::Json),
98 opts.verbose,
99 ),
100 [name] => create(&mkit_dir, name),
101 _ => super::usage_error("usage: mkit branch <name> (create takes one name)"),
102 }
103}
104
105fn create(mkit_dir: &std::path::Path, name: &str) -> u8 {
107 let Ok(Some(h)) = refs::resolve_head(mkit_dir) else {
108 return emit_err("no HEAD commit to branch from", exit::GENERAL_ERROR);
109 };
110 match super::write_ref_recording_history(mkit_dir, name, refs::RefWriteCondition::Missing, &h) {
116 Ok(()) => exit::OK,
117 Err(refs::RefError::Conflict(_)) => {
118 emit_err(&format!("branch '{name}' already exists"), exit::CANTCREAT)
119 }
120 Err(e) => emit_err(&format!("write {name}: {e}"), exit::CANTCREAT),
121 }
122}
123
124fn delete(mkit_dir: &std::path::Path, names: &[String], force: bool) -> u8 {
134 let [name] = names else {
135 let flag = if force { "-D" } else { "-d" };
136 return super::usage_error(&format!("usage: mkit branch {flag} <name>"));
137 };
138 match refs::delete_ref_safe(mkit_dir, name) {
139 Ok(()) => exit::OK,
140 Err(refs::RefError::NotFound(_)) => {
141 emit_err(&format!("branch '{name}' not found"), exit::GENERAL_ERROR)
142 }
143 Err(e) => emit_err(&format!("delete {name}: {e}"), exit::GENERAL_ERROR),
144 }
145}
146
147fn rename(mkit_dir: &std::path::Path, names: &[String]) -> u8 {
158 let (old, new) = match names {
159 [new] => {
160 let Ok(refs::Head::Branch(cur)) = refs::read_head(mkit_dir) else {
161 return emit_err(
162 "cannot rename: HEAD is detached (specify <old> <new>)",
163 exit::GENERAL_ERROR,
164 );
165 };
166 (cur, new.clone())
167 }
168 [old, new] => (old.clone(), new.clone()),
169 _ => return super::usage_error("usage: mkit branch -m [<old>] <new>"),
170 };
171
172 if old == new {
173 return exit::OK;
174 }
175
176 let hash = match refs::read_ref(mkit_dir, &old) {
177 Ok(Some(h)) => h,
178 Ok(None) => return emit_err(&format!("branch '{old}' not found"), exit::GENERAL_ERROR),
179 Err(e) => return emit_err(&format!("read {old}: {e}"), exit::GENERAL_ERROR),
180 };
181
182 match super::write_ref_recording_history(
186 mkit_dir,
187 &new,
188 refs::RefWriteCondition::Missing,
189 &hash,
190 ) {
191 Ok(()) => {}
192 Err(refs::RefError::Conflict(_)) => {
193 return emit_err(&format!("branch '{new}' already exists"), exit::CANTCREAT);
194 }
195 Err(e) => return emit_err(&format!("write {new}: {e}"), exit::CANTCREAT),
196 }
197
198 if let Err(e) = refs::delete_ref(mkit_dir, &old) {
199 return emit_err(&format!("delete {old}: {e}"), exit::GENERAL_ERROR);
200 }
201
202 if let Ok(refs::Head::Branch(cur)) = refs::read_head(mkit_dir)
204 && cur == old
205 && let Err(e) = refs::write_head_branch(mkit_dir, &new)
206 {
207 return emit_err(&format!("update HEAD to {new}: {e}"), exit::GENERAL_ERROR);
208 }
209 exit::OK
210}
211
212fn list(cwd: &std::path::Path, mkit_dir: &std::path::Path, json: bool, verbose: bool) -> u8 {
213 let current = match refs::read_head(mkit_dir) {
214 Ok(Head::Branch(n)) => Some(n),
215 _ => None,
216 };
217 let refs = match refs::list_refs(mkit_dir) {
218 Ok(r) => r,
219 Err(e) => return emit_err(&format!("list refs: {e}"), exit::GENERAL_ERROR),
220 };
221 let mut stdout = std::io::stdout().lock();
222 if json {
223 for r in &refs {
224 let is_current = current.as_deref() == Some(r.name.as_str());
225 let _ = stdout.write_all(b"{");
226 let _ = write!(stdout, "\"name\":\"{}\"", format::json_escape(&r.name));
227 let _ = write!(stdout, ",\"current\":{is_current}");
228 if let Some(h) = &r.hash {
229 let _ = write!(stdout, ",\"hash\":\"{}\"", format::hex_hash(h));
230 } else {
231 let _ = stdout.write_all(b",\"hash\":null");
232 }
233 let _ = stdout.write_all(b"}\n");
234 }
235 return exit::OK;
236 }
237
238 let marker_for = |name: &str| {
239 current
240 .as_deref()
241 .map_or(' ', |cur| if cur == name { '*' } else { ' ' })
242 };
243
244 if !verbose {
245 for r in &refs {
247 let _ = writeln!(stdout, "{} {}", marker_for(&r.name), r.name);
248 }
249 return exit::OK;
250 }
251
252 let store = match ObjectStore::open(cwd) {
256 Ok(s) => s,
257 Err(e) => return emit_err(&format!("open store: {e}"), exit::GENERAL_ERROR),
258 };
259 let width = refs.iter().map(|r| r.name.len()).max().unwrap_or(0);
260 for r in &refs {
261 let marker = marker_for(&r.name);
262 match &r.hash {
263 Some(h) => {
264 let short = format::short_hash(h, DEFAULT_ABBREV);
265 let subject = tip_subject(&store, h);
266 let _ = writeln!(stdout, "{marker} {:<width$} {short} {subject}", r.name);
267 }
268 None => {
269 let _ = writeln!(stdout, "{marker} {:<width$}", r.name);
270 }
271 }
272 }
273 exit::OK
274}
275
276fn tip_subject(store: &ObjectStore, hash: &mkit_core::hash::Hash) -> String {
280 let message = match store.read_object(hash) {
281 Ok(Object::Commit(c)) => c.message,
282 Ok(Object::Remix(r)) => r.message,
283 _ => return String::new(),
284 };
285 String::from_utf8_lossy(&message)
286 .lines()
287 .next()
288 .unwrap_or("")
289 .to_owned()
290}
291
292fn emit_err(msg: &str, code: u8) -> u8 {
293 let mut stderr = std::io::stderr().lock();
294 let _ = writeln!(stderr, "error: {msg}");
295 code
296}