1use std::fmt;
2use std::str::FromStr;
3
4use sha1::{Digest, Sha1};
5
6use crate::error::{Error, Result};
7
8#[non_exhaustive]
10#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
11pub enum TargetType {
12 Commit,
13 ChangeId,
14 Branch,
15 Path,
16 Project,
17}
18
19impl fmt::Display for TargetType {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 f.write_str(self.as_str())
22 }
23}
24
25impl FromStr for TargetType {
26 type Err = Error;
27
28 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
29 match s {
30 "commit" => Ok(TargetType::Commit),
31 "change-id" => Ok(TargetType::ChangeId),
32 "branch" => Ok(TargetType::Branch),
33 "path" => Ok(TargetType::Path),
34 "project" => Ok(TargetType::Project),
35 _ => Err(Error::UnknownTargetType(s.to_string())),
36 }
37 }
38}
39
40impl TargetType {
41 pub fn as_str(&self) -> &str {
43 match self {
44 TargetType::Commit => "commit",
45 TargetType::ChangeId => "change-id",
46 TargetType::Branch => "branch",
47 TargetType::Path => "path",
48 TargetType::Project => "project",
49 }
50 }
51
52 pub fn pluralize(&self) -> &str {
54 match self {
55 TargetType::Commit => "commits",
56 TargetType::ChangeId => "change-ids",
57 TargetType::Branch => "branches",
58 TargetType::Path => "paths",
59 TargetType::Project => "project",
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Hash)]
66pub struct Target {
67 target_type: TargetType,
68 value: Option<String>,
69}
70
71impl fmt::Display for Target {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 match &self.value {
74 Some(v) => write!(f, "{}:{}", self.target_type, v),
75 None => write!(f, "{}", self.target_type),
76 }
77 }
78}
79
80impl Target {
81 #[must_use]
93 pub fn from_parts(target_type: TargetType, value: Option<String>) -> Self {
94 Target { target_type, value }
95 }
96
97 pub fn commit(sha: &str) -> Result<Self> {
105 Self::parse(&format!("commit:{sha}"))
106 }
107
108 #[must_use]
110 pub fn project() -> Self {
111 Target {
112 target_type: TargetType::Project,
113 value: None,
114 }
115 }
116
117 #[must_use]
122 pub fn path(path: &str) -> Self {
123 Target {
124 target_type: TargetType::Path,
125 value: Some(path.to_string()),
126 }
127 }
128
129 #[must_use]
134 pub fn branch(name: &str) -> Self {
135 Target {
136 target_type: TargetType::Branch,
137 value: Some(name.to_string()),
138 }
139 }
140
141 #[must_use]
146 pub fn change_id(id: &str) -> Self {
147 Target {
148 target_type: TargetType::ChangeId,
149 value: Some(id.to_string()),
150 }
151 }
152
153 pub fn parse(s: &str) -> Result<Self> {
166 if s == "project" {
167 return Ok(Target {
168 target_type: TargetType::Project,
169 value: None,
170 });
171 }
172
173 let (type_str, value) = s.split_once(':').ok_or_else(|| {
174 Error::InvalidTarget("target must be in type:value format (e.g. commit:abc123)".into())
175 })?;
176
177 let target_type = type_str.parse::<TargetType>()?;
178
179 if target_type == TargetType::Project {
180 return Ok(Target {
181 target_type,
182 value: None,
183 });
184 }
185
186 if value.len() < 3 {
187 return Err(Error::InvalidTarget(format!(
188 "target value must be at least 3 characters, got: {value}"
189 )));
190 }
191
192 Ok(Target {
193 target_type,
194 value: Some(value.to_string()),
195 })
196 }
197
198 #[must_use]
200 pub fn target_type(&self) -> &TargetType {
201 &self.target_type
202 }
203
204 #[must_use]
208 pub fn value(&self) -> Option<&str> {
209 self.value.as_deref()
210 }
211
212 pub fn resolve(&self, repo: &gix::Repository) -> Result<Target> {
216 if self.target_type == TargetType::Commit {
217 if let Some(ref v) = self.value {
218 if v.len() < 40 {
219 let full = crate::git_utils::resolve_commit_sha(repo, v)?;
220 return Ok(Target {
221 target_type: self.target_type.clone(),
222 value: Some(full),
223 });
224 }
225 }
226 }
227 Ok(self.clone())
228 }
229}
230
231#[non_exhaustive]
233#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
234pub enum ValueType {
235 String,
236 List,
237 Set,
238}
239
240impl fmt::Display for ValueType {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 f.write_str(self.as_str())
243 }
244}
245
246impl FromStr for ValueType {
247 type Err = Error;
248
249 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
250 match s {
251 "string" => Ok(ValueType::String),
252 "list" => Ok(ValueType::List),
253 "set" => Ok(ValueType::Set),
254 _ => Err(Error::UnknownValueType(s.to_string())),
255 }
256 }
257}
258
259impl ValueType {
260 pub fn as_str(&self) -> &str {
262 match self {
263 ValueType::String => "string",
264 ValueType::List => "list",
265 ValueType::Set => "set",
266 }
267 }
268}
269
270#[derive(Debug, Clone, PartialEq, Eq)]
276#[non_exhaustive]
277pub enum MetaValue {
278 String(String),
280 List(Vec<crate::list_value::ListEntry>),
282 Set(std::collections::BTreeSet<String>),
284}
285
286impl fmt::Display for MetaValue {
287 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288 match self {
289 MetaValue::String(s) => write!(f, "{s}"),
290 MetaValue::List(entries) => write!(f, "[{} entries]", entries.len()),
291 MetaValue::Set(members) => write!(f, "{{{} members}}", members.len()),
292 }
293 }
294}
295
296impl MetaValue {
297 #[must_use]
299 pub fn value_type(&self) -> ValueType {
300 match self {
301 MetaValue::String(_) => ValueType::String,
302 MetaValue::List(_) => ValueType::List,
303 MetaValue::Set(_) => ValueType::Set,
304 }
305 }
306}
307
308impl From<&str> for MetaValue {
309 fn from(s: &str) -> Self {
310 MetaValue::String(s.to_string())
311 }
312}
313
314impl From<String> for MetaValue {
315 fn from(s: String) -> Self {
316 MetaValue::String(s)
317 }
318}
319
320impl From<Vec<crate::list_value::ListEntry>> for MetaValue {
321 fn from(entries: Vec<crate::list_value::ListEntry>) -> Self {
322 MetaValue::List(entries)
323 }
324}
325
326impl From<std::collections::BTreeSet<String>> for MetaValue {
327 fn from(members: std::collections::BTreeSet<String>) -> Self {
328 MetaValue::Set(members)
329 }
330}
331
332#[cfg(not(feature = "internal"))]
334pub(crate) const GIT_REF_THRESHOLD: usize = 1024;
335#[cfg(feature = "internal")]
337pub const GIT_REF_THRESHOLD: usize = 1024;
338
339pub(crate) const STRING_VALUE_BLOB: &str = "__value";
341
342pub(crate) const LIST_VALUE_DIR: &str = "__list";
344
345pub(crate) const SET_VALUE_DIR: &str = "__set";
347
348pub(crate) const TOMBSTONE_ROOT: &str = "__tombstones";
350
351pub(crate) const TOMBSTONE_BLOB: &str = "__deleted";
353
354pub(crate) const PATH_TARGET_SEPARATOR: &str = "__target__";
356
357pub(crate) fn decode_path_target_segments(segments: &[&str]) -> Result<String> {
359 if segments.is_empty() {
360 return Err(Error::InvalidTreePath(
361 "path target must include at least one segment".into(),
362 ));
363 }
364
365 let decoded = segments
366 .iter()
367 .map(|segment| {
368 if let Some(rest) = segment.strip_prefix('~') {
369 rest.to_string()
370 } else {
371 (*segment).to_string()
372 }
373 })
374 .collect::<Vec<_>>()
375 .join("/");
376
377 Ok(decoded)
378}
379
380pub(crate) fn set_member_id(value: &str) -> String {
382 let header = format!("blob {}\0", value.len());
383 let mut hasher = Sha1::new();
384 hasher.update(header.as_bytes());
385 hasher.update(value.as_bytes());
386 format!("{:x}", hasher.finalize())
387}
388
389fn validate_key_segment(segment: &str) -> Result<()> {
390 if segment.is_empty() {
391 return Err(Error::InvalidKey("key segments cannot be empty".into()));
392 }
393 if segment == "." || segment == ".." {
394 return Err(Error::InvalidKey(format!(
395 "key segment '{segment}' is not allowed"
396 )));
397 }
398 if segment.contains('/') {
399 return Err(Error::InvalidKey(format!(
400 "key segment '{segment}' must not contain '/'"
401 )));
402 }
403 if segment.contains('\0') {
404 return Err(Error::InvalidKey(format!(
405 "key segment '{segment}' must not contain null byte"
406 )));
407 }
408 if segment.starts_with("__")
409 || segment == STRING_VALUE_BLOB
410 || segment == LIST_VALUE_DIR
411 || segment == SET_VALUE_DIR
412 {
413 return Err(Error::InvalidKey(format!(
414 "key segment '{segment}' is reserved"
415 )));
416 }
417 Ok(())
418}
419
420#[cfg(not(feature = "internal"))]
426pub(crate) fn validate_key(key: &str) -> Result<()> {
427 validate_key_inner(key)
428}
429
430#[cfg(feature = "internal")]
436pub fn validate_key(key: &str) -> Result<()> {
437 validate_key_inner(key)
438}
439
440fn validate_key_inner(key: &str) -> Result<()> {
441 if key.is_empty() {
442 return Err(Error::InvalidKey("key cannot be empty".into()));
443 }
444 for segment in key.split(':') {
445 validate_key_segment(segment)?;
446 }
447 Ok(())
448}
449
450pub(crate) fn decode_key_path_segments(segments: &[&str]) -> Result<String> {
452 if segments.is_empty() {
453 return Err(Error::InvalidKey(
454 "key path must include at least one key segment".into(),
455 ));
456 }
457 let mut decoded = Vec::with_capacity(segments.len());
458 for segment in segments {
459 validate_key_segment(segment)?;
460 decoded.push((*segment).to_string());
461 }
462 Ok(decoded.join(":"))
463}
464
465#[cfg(test)]
466#[allow(clippy::unwrap_used, clippy::expect_used)]
467mod tests {
468 use super::*;
469
470 #[test]
471 fn test_parse_commit_target() {
472 let t = Target::parse("commit:abc123").unwrap();
473 assert_eq!(t.target_type(), &TargetType::Commit);
474 assert_eq!(t.value(), Some("abc123"));
475 }
476
477 #[test]
478 fn test_parse_project_target() {
479 let t = Target::parse("project").unwrap();
480 assert_eq!(t.target_type(), &TargetType::Project);
481 assert_eq!(t.value(), None);
482 }
483
484 #[test]
485 fn test_parse_path_target_with_colon_in_value() {
486 let t = Target::parse("path:src/foo.rs").unwrap();
488 assert_eq!(t.target_type(), &TargetType::Path);
489 assert_eq!(t.value(), Some("src/foo.rs"));
490 }
491
492 #[test]
493 fn test_parse_short_value_rejected() {
494 let result = Target::parse("commit:ab");
495 assert!(result.is_err());
496 }
497
498 #[test]
499 fn test_parse_unknown_type_rejected() {
500 let result = Target::parse("unknown:abc123");
501 assert!(result.is_err());
502 }
503
504 #[test]
505 fn test_value_type_roundtrip() {
506 assert_eq!("string".parse::<ValueType>().unwrap(), ValueType::String);
507 assert_eq!("list".parse::<ValueType>().unwrap(), ValueType::List);
508 assert_eq!("set".parse::<ValueType>().unwrap(), ValueType::Set);
509 assert!("hash".parse::<ValueType>().is_err());
510 }
511
512 #[test]
513 fn test_parse_branch_target() {
514 let t = Target::parse("branch:sc-branch-1-deadbeef").unwrap();
515 assert_eq!(t.target_type(), &TargetType::Branch);
516 assert_eq!(t.value(), Some("sc-branch-1-deadbeef"));
517 }
518
519 #[test]
520 fn test_decode_path_target_segments() {
521 let decoded =
522 super::decode_path_target_segments(&["src", "~__generated", "file.rs"]).unwrap();
523 assert_eq!(decoded, "src/__generated/file.rs");
524 }
525
526 #[test]
527 fn test_decode_key_path_segments() {
528 let decoded = super::decode_key_path_segments(&["agent", "model"]).unwrap();
529 assert_eq!(decoded, "agent:model");
530 }
531
532 #[test]
533 fn test_validate_key_rejects_reserved_segments() {
534 assert!(super::validate_key("agent:__value").is_err());
535 assert!(super::validate_key("__list:chat").is_err());
536 assert!(super::validate_key("__custom:model").is_err());
537 }
538
539 #[test]
540 fn test_validate_key_rejects_unsafe_segments() {
541 assert!(super::validate_key("agent:/model").is_err());
542 assert!(super::validate_key("agent::model").is_err());
543 assert!(super::validate_key("agent:.").is_err());
544 assert!(super::validate_key("agent:..").is_err());
545 }
546
547 #[test]
548 fn test_validate_key_accepts_normal_segments() {
549 assert!(super::validate_key("agent:model:version").is_ok());
550 }
551
552 #[test]
553 fn test_meta_value_string_type() {
554 let v = MetaValue::String("hello".to_string());
555 assert_eq!(v.value_type(), ValueType::String);
556 }
557
558 #[test]
559 fn test_meta_value_list_type() {
560 let v = MetaValue::List(vec![crate::list_value::ListEntry {
561 value: "item".to_string(),
562 timestamp: 1000,
563 }]);
564 assert_eq!(v.value_type(), ValueType::List);
565 }
566
567 #[test]
568 fn test_meta_value_set_type() {
569 let mut s = std::collections::BTreeSet::new();
570 s.insert("a".to_string());
571 s.insert("b".to_string());
572 let v = MetaValue::Set(s);
573 assert_eq!(v.value_type(), ValueType::Set);
574 }
575
576 #[test]
577 fn test_meta_value_empty_list_type() {
578 let v = MetaValue::List(vec![]);
579 assert_eq!(v.value_type(), ValueType::List);
580 }
581
582 #[test]
583 fn test_meta_value_empty_set_type() {
584 let v = MetaValue::Set(std::collections::BTreeSet::new());
585 assert_eq!(v.value_type(), ValueType::Set);
586 }
587
588 #[test]
589 fn test_meta_value_clone_eq() {
590 let v1 = MetaValue::String("test".to_string());
591 let v2 = v1.clone();
592 assert_eq!(v1, v2);
593 }
594
595 #[test]
596 fn test_target_commit_constructor() {
597 let t = Target::commit("abc123").unwrap();
598 assert_eq!(t.target_type(), &TargetType::Commit);
599 assert_eq!(t.value(), Some("abc123"));
600 }
601
602 #[test]
603 fn test_target_commit_constructor_short_sha_rejected() {
604 let result = Target::commit("ab");
605 assert!(result.is_err());
606 }
607
608 #[test]
609 fn test_target_project_constructor() {
610 let t = Target::project();
611 assert_eq!(t.target_type(), &TargetType::Project);
612 assert_eq!(t.value(), None);
613 }
614
615 #[test]
616 fn test_target_path_constructor() {
617 let t = Target::path("src/main.rs");
618 assert_eq!(t.target_type(), &TargetType::Path);
619 assert_eq!(t.value(), Some("src/main.rs"));
620 }
621
622 #[test]
623 fn test_target_branch_constructor() {
624 let t = Target::branch("feature-x");
625 assert_eq!(t.target_type(), &TargetType::Branch);
626 assert_eq!(t.value(), Some("feature-x"));
627 }
628
629 #[test]
630 fn test_target_change_id_constructor() {
631 let t = Target::change_id("jj-change-abc");
632 assert_eq!(t.target_type(), &TargetType::ChangeId);
633 assert_eq!(t.value(), Some("jj-change-abc"));
634 }
635
636 #[test]
637 fn test_named_constructors_match_parse() {
638 let from_parse = Target::parse("commit:abc123").unwrap();
640 let from_ctor = Target::commit("abc123").unwrap();
641 assert_eq!(from_parse, from_ctor);
642
643 let from_parse = Target::parse("project").unwrap();
644 let from_ctor = Target::project();
645 assert_eq!(from_parse, from_ctor);
646
647 let from_parse = Target::parse("path:src/main.rs").unwrap();
648 let from_ctor = Target::path("src/main.rs");
649 assert_eq!(from_parse, from_ctor);
650
651 let from_parse = Target::parse("branch:feature-x").unwrap();
652 let from_ctor = Target::branch("feature-x");
653 assert_eq!(from_parse, from_ctor);
654
655 let from_parse = Target::parse("change-id:jj-change-abc").unwrap();
656 let from_ctor = Target::change_id("jj-change-abc");
657 assert_eq!(from_parse, from_ctor);
658 }
659}