Skip to main content

mkit_cli/commands/
symbolic_ref.rs

1//! `mkit symbolic-ref [--short] <name> [<ref>]` — read or write a symbolic
2//! ref (currently only `HEAD`), like `git symbolic-ref`.
3//!
4//! - Read: `symbolic-ref HEAD` prints the full target ref
5//!   (`refs/heads/main`), or just the branch with `--short`. Errors when
6//!   HEAD is detached / not symbolic, like git.
7//! - Write: `symbolic-ref HEAD refs/heads/<branch>` repoints HEAD at a
8//!   branch (the plumbing form — it does not touch the worktree). The target
9//!   must be under `refs/heads/`; the branch need not exist yet (matching
10//!   git).
11
12use std::io::Write;
13
14use clap::Parser;
15use mkit_core::layout::RepoLayout;
16use mkit_core::refs::{self, Head};
17
18use crate::clap_shim;
19use crate::exit;
20
21#[derive(Debug, Parser)]
22#[command(
23    name = "mkit symbolic-ref",
24    about = "Read or write a symbolic ref (e.g. HEAD)."
25)]
26struct SymbolicRefOpts {
27    /// Print the short ref name (`main`) instead of `refs/heads/main`.
28    #[arg(long)]
29    short: bool,
30    /// The symbolic ref to read or write (currently only `HEAD`).
31    name: String,
32    /// When given, the target ref to point `<name>` at (write mode), e.g.
33    /// `refs/heads/main`.
34    target: Option<String>,
35}
36
37#[must_use]
38pub fn run(args: &[String]) -> u8 {
39    let opts = match clap_shim::parse::<SymbolicRefOpts>("mkit symbolic-ref", args) {
40        Ok(o) => o,
41        Err(code) => return code,
42    };
43    if opts.name != "HEAD" {
44        return emit_err(
45            &format!("only HEAD is a symbolic ref in mkit (got '{}')", opts.name),
46            exit::GENERAL_ERROR,
47        );
48    }
49    let cwd = match std::env::current_dir() {
50        Ok(p) => p,
51        Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
52    };
53    let layout = match super::resolve_layout(&cwd) {
54        Ok(layout) => layout,
55        Err(code) => return code,
56    };
57
58    match opts.target {
59        Some(target) => write_head(&layout, &target),
60        None => read_head(&layout, opts.short),
61    }
62}
63
64/// Read mode: print HEAD's target (full ref, or short branch name).
65fn read_head(layout: &RepoLayout, short: bool) -> u8 {
66    match refs::read_head(layout) {
67        Ok(Head::Branch(name)) => {
68            let mut stdout = std::io::stdout().lock();
69            if short {
70                let _ = writeln!(stdout, "{name}");
71            } else {
72                let _ = writeln!(stdout, "refs/heads/{name}");
73            }
74            exit::OK
75        }
76        // Detached HEAD: not a symbolic ref (git errors here too).
77        Ok(Head::Detached(_)) => emit_err("ref HEAD is not a symbolic ref", exit::GENERAL_ERROR),
78        Err(e) => emit_err(&format!("read HEAD: {e}"), exit::DATAERR),
79    }
80}
81
82/// Write mode: repoint HEAD at `<target>` (must be `refs/heads/<branch>`).
83/// Like git, the branch need not exist yet, and the worktree is untouched.
84fn write_head(layout: &RepoLayout, target: &str) -> u8 {
85    let Some(branch) = target.strip_prefix("refs/heads/") else {
86        return emit_err(
87            &format!("HEAD can only point at a branch under refs/heads/ (got '{target}')"),
88            exit::USAGE,
89        );
90    };
91    if !refs::validate_ref_name(branch) {
92        return emit_err(&format!("invalid branch name '{branch}'"), exit::USAGE);
93    }
94    match refs::write_head_branch(layout, branch) {
95        Ok(()) => exit::OK,
96        Err(e) => emit_err(&format!("write HEAD: {e}"), exit::CANTCREAT),
97    }
98}
99
100use super::error as emit_err;