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