1use std::io::Write;
20
21use clap::Parser;
22use mkit_core::hash::Hash;
23use mkit_core::refs::{self, RefWriteCondition};
24use mkit_core::store::ObjectStore;
25
26use super::revspec;
27use crate::clap_shim;
28use crate::exit;
29
30#[derive(Debug, Parser)]
31#[command(
32 name = "mkit update-ref",
33 about = "Create, update, or delete a ref (guarded)."
34)]
35struct UpdateRefOpts {
36 #[arg(short = 'd', long)]
38 delete: bool,
39 name: String,
41 value: Option<String>,
44 old_value: Option<String>,
46}
47
48enum Namespace {
50 Head,
51 Tag,
52}
53
54#[must_use]
55pub fn run(args: &[String]) -> u8 {
56 let opts = match clap_shim::parse::<UpdateRefOpts>("mkit update-ref", args) {
57 Ok(o) => o,
58 Err(code) => return code,
59 };
60 let cwd = match std::env::current_dir() {
61 Ok(p) => p,
62 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
63 };
64 let store = match ObjectStore::open(&cwd) {
65 Ok(s) => s,
66 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
67 };
68 let mkit_dir = cwd.join(mkit_core::MKIT_DIR);
69
70 let Some((ns, name)) = parse_ref(&opts.name) else {
71 return emit_err(
72 &format!(
73 "unsupported ref '{}': update-ref handles refs/heads/<branch> and refs/tags/<name>",
74 opts.name
75 ),
76 exit::USAGE,
77 );
78 };
79
80 let _lock = match super::acquire_worktree_lock(&cwd) {
87 Ok(l) => l,
88 Err(code) => return code,
89 };
90
91 if opts.delete {
92 if opts.old_value.is_some() {
93 return super::usage_error("usage: mkit update-ref -d <ref> [<oldvalue>]");
94 }
95 return run_delete(&store, &mkit_dir, &ns, name, opts.value.as_deref());
96 }
97
98 let Some(newspec) = opts.value.as_deref() else {
99 return super::usage_error("usage: mkit update-ref <ref> <newvalue> [<oldvalue>]");
100 };
101 let newhash = match resolve(&store, &mkit_dir, newspec) {
102 Ok(h) => h,
103 Err(msg) => return emit_err(&msg, exit::DATAERR),
104 };
105 if !store.contains(&newhash) {
108 return emit_err(
109 &format!("object '{newspec}' does not exist in the store"),
110 exit::DATAERR,
111 );
112 }
113 let condition = match opts.old_value.as_deref() {
114 None => RefWriteCondition::Any,
115 Some(s) if is_zero(s) => RefWriteCondition::Missing,
116 Some(s) => match resolve(&store, &mkit_dir, s) {
117 Ok(h) => RefWriteCondition::Match(h),
118 Err(msg) => return emit_err(&msg, exit::DATAERR),
119 },
120 };
121 let res = match ns {
122 Namespace::Head => super::write_ref_recording_history(&mkit_dir, name, condition, &newhash),
127 Namespace::Tag => refs::update_tag(&mkit_dir, name, condition, &newhash),
128 };
129 match res {
130 Ok(()) => exit::OK,
131 Err(e) => emit_err(
132 &format!("update-ref {}: {e}", opts.name),
133 exit::GENERAL_ERROR,
134 ),
135 }
136}
137
138fn run_delete(
140 store: &ObjectStore,
141 mkit_dir: &std::path::Path,
142 ns: &Namespace,
143 name: &str,
144 old_value: Option<&str>,
145) -> u8 {
146 if let Some(spec) = old_value {
147 if is_zero(spec) {
148 return emit_err(
149 "cannot delete a ref whose expected old value is all-zero (absent)",
150 exit::USAGE,
151 );
152 }
153 let expected = match resolve(store, mkit_dir, spec) {
154 Ok(h) => h,
155 Err(msg) => return emit_err(&msg, exit::DATAERR),
156 };
157 let current = match ns {
158 Namespace::Head => refs::read_ref(mkit_dir, name),
159 Namespace::Tag => refs::read_tag(mkit_dir, name),
160 };
161 match current {
162 Ok(Some(h)) if h == expected => {}
163 Ok(_) => {
164 return emit_err(
165 "ref does not have the expected old value; not deleting",
166 exit::GENERAL_ERROR,
167 );
168 }
169 Err(e) => return emit_err(&format!("read ref: {e}"), exit::GENERAL_ERROR),
170 }
171 }
172 let res = match ns {
173 Namespace::Head => refs::delete_ref_safe(mkit_dir, name),
175 Namespace::Tag => refs::delete_tag(mkit_dir, name),
176 };
177 match res {
178 Ok(()) => exit::OK,
179 Err(e) => emit_err(&format!("delete ref: {e}"), exit::GENERAL_ERROR),
180 }
181}
182
183fn parse_ref(full: &str) -> Option<(Namespace, &str)> {
186 if let Some(b) = full.strip_prefix("refs/heads/") {
187 Some((Namespace::Head, b))
188 } else if let Some(t) = full.strip_prefix("refs/tags/") {
189 Some((Namespace::Tag, t))
190 } else {
191 None
192 }
193}
194
195fn resolve(store: &ObjectStore, mkit_dir: &std::path::Path, spec: &str) -> Result<Hash, String> {
197 revspec::resolve_revision(store, mkit_dir, spec)
198 .map_err(|e| format!("bad revision '{spec}': {e}"))
199}
200
201fn is_zero(s: &str) -> bool {
203 s.len() == 64 && s.bytes().all(|b| b == b'0')
204}
205
206fn emit_err(msg: &str, code: u8) -> u8 {
207 let mut stderr = std::io::stderr().lock();
208 let _ = writeln!(stderr, "error: {msg}");
209 code
210}