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,
14 ChangeId,
16 Branch,
18 Path,
20 Project,
22}
23
24impl fmt::Display for TargetType {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 f.write_str(self.as_str())
27 }
28}
29
30impl FromStr for TargetType {
31 type Err = Error;
32
33 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
34 match s {
35 "commit" => Ok(TargetType::Commit),
36 "change-id" => Ok(TargetType::ChangeId),
37 "branch" => Ok(TargetType::Branch),
38 "path" => Ok(TargetType::Path),
39 "project" => Ok(TargetType::Project),
40 _ => Err(Error::UnknownTargetType(s.to_string())),
41 }
42 }
43}
44
45impl TargetType {
46 #[must_use]
48 pub fn as_str(&self) -> &str {
49 match self {
50 TargetType::Commit => "commit",
51 TargetType::ChangeId => "change-id",
52 TargetType::Branch => "branch",
53 TargetType::Path => "path",
54 TargetType::Project => "project",
55 }
56 }
57
58 #[must_use]
60 pub fn pluralize(&self) -> &str {
61 match self {
62 TargetType::Commit => "commits",
63 TargetType::ChangeId => "change-ids",
64 TargetType::Branch => "branches",
65 TargetType::Path => "paths",
66 TargetType::Project => "project",
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Hash)]
73#[must_use]
74pub struct Target {
75 target_type: TargetType,
76 value: Option<String>,
77}
78
79impl fmt::Display for Target {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 match &self.value {
82 Some(v) => write!(f, "{}:{}", self.target_type, v),
83 None => write!(f, "{}", self.target_type),
84 }
85 }
86}
87
88impl Target {
89 pub fn from_parts(target_type: TargetType, value: Option<String>) -> Self {
101 Target { target_type, value }
102 }
103
104 pub fn commit(sha: &str) -> Result<Self> {
112 Self::parse(&format!("commit:{sha}"))
113 }
114
115 pub fn project() -> Self {
117 Target {
118 target_type: TargetType::Project,
119 value: None,
120 }
121 }
122
123 pub fn path(path: &str) -> Self {
128 Target {
129 target_type: TargetType::Path,
130 value: Some(path.to_string()),
131 }
132 }
133
134 pub fn branch(name: &str) -> Self {
139 Target {
140 target_type: TargetType::Branch,
141 value: Some(name.to_string()),
142 }
143 }
144
145 pub fn change_id(id: &str) -> Self {
150 Target {
151 target_type: TargetType::ChangeId,
152 value: Some(id.to_string()),
153 }
154 }
155
156 pub fn parse(s: &str) -> Result<Self> {
169 if s == "project" {
170 return Ok(Target {
171 target_type: TargetType::Project,
172 value: None,
173 });
174 }
175
176 let (type_str, value) = s.split_once(':').ok_or_else(|| {
177 Error::InvalidTarget("target must be in type:value format (e.g. commit:abc123)".into())
178 })?;
179
180 let target_type = type_str.parse::<TargetType>()?;
181
182 if target_type == TargetType::Project {
183 return Ok(Target {
184 target_type,
185 value: None,
186 });
187 }
188
189 if value.len() < 3 {
190 return Err(Error::InvalidTarget(format!(
191 "target value must be at least 3 characters, got: {value}"
192 )));
193 }
194
195 Ok(Target {
196 target_type,
197 value: Some(value.to_string()),
198 })
199 }
200
201 #[must_use]
203 pub fn target_type(&self) -> &TargetType {
204 &self.target_type
205 }
206
207 #[must_use]
211 pub fn value(&self) -> Option<&str> {
212 self.value.as_deref()
213 }
214
215 pub fn resolve(&self, repo: &gix::Repository) -> Result<Target> {
219 if self.target_type == TargetType::Commit {
220 if let Some(ref v) = self.value {
221 if v.len() < 40 {
222 let full = crate::git_utils::resolve_commit_sha(repo, v)?;
223 return Ok(Target {
224 target_type: self.target_type.clone(),
225 value: Some(full),
226 });
227 }
228 }
229 }
230 Ok(self.clone())
231 }
232}
233
234#[non_exhaustive]
236#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
237pub enum ValueType {
238 String,
240 List,
242 Set,
244}
245
246impl fmt::Display for ValueType {
247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248 f.write_str(self.as_str())
249 }
250}
251
252impl FromStr for ValueType {
253 type Err = Error;
254
255 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
256 match s {
257 "string" => Ok(ValueType::String),
258 "list" => Ok(ValueType::List),
259 "set" => Ok(ValueType::Set),
260 _ => Err(Error::UnknownValueType(s.to_string())),
261 }
262 }
263}
264
265impl ValueType {
266 #[must_use]
268 pub fn as_str(&self) -> &str {
269 match self {
270 ValueType::String => "string",
271 ValueType::List => "list",
272 ValueType::Set => "set",
273 }
274 }
275}
276
277#[derive(Debug, Clone, PartialEq, Eq)]
283#[non_exhaustive]
284pub enum MetaValue {
285 String(String),
287 List(Vec<crate::ListEntry>),
289 Set(std::collections::BTreeSet<String>),
291}
292
293impl fmt::Display for MetaValue {
294 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295 match self {
296 MetaValue::String(s) => write!(f, "{s}"),
297 MetaValue::List(entries) => write!(f, "[{} entries]", entries.len()),
298 MetaValue::Set(members) => write!(f, "{{{} members}}", members.len()),
299 }
300 }
301}
302
303impl MetaValue {
304 #[must_use]
306 pub fn value_type(&self) -> ValueType {
307 match self {
308 MetaValue::String(_) => ValueType::String,
309 MetaValue::List(_) => ValueType::List,
310 MetaValue::Set(_) => ValueType::Set,
311 }
312 }
313}
314
315impl From<&str> for MetaValue {
316 fn from(s: &str) -> Self {
317 MetaValue::String(s.to_string())
318 }
319}
320
321impl From<String> for MetaValue {
322 fn from(s: String) -> Self {
323 MetaValue::String(s)
324 }
325}
326
327impl From<Vec<crate::ListEntry>> for MetaValue {
328 fn from(entries: Vec<crate::ListEntry>) -> Self {
329 MetaValue::List(entries)
330 }
331}
332
333impl From<std::collections::BTreeSet<String>> for MetaValue {
334 fn from(members: std::collections::BTreeSet<String>) -> Self {
335 MetaValue::Set(members)
336 }
337}
338
339#[derive(Debug, Clone)]
341#[non_exhaustive]
342#[must_use]
343pub enum MetaEdit<'a> {
344 ListAppend {
346 key: &'a str,
348 entries: &'a [crate::ListEntry],
350 },
351 SetAdd {
353 key: &'a str,
355 members: &'a [String],
357 },
358 SetValue {
360 key: &'a str,
362 value: &'a crate::MetaValue,
364 },
365}
366
367impl<'a> MetaEdit<'a> {
368 pub fn list_append(key: &'a str, entries: &'a [crate::ListEntry]) -> Self {
374 Self::ListAppend { key, entries }
375 }
376
377 pub fn set_add(key: &'a str, members: &'a [String]) -> Self {
379 Self::SetAdd { key, members }
380 }
381
382 pub fn set_value(key: &'a str, value: &'a crate::MetaValue) -> Self {
384 Self::SetValue { key, value }
385 }
386}
387
388#[cfg(not(feature = "internal"))]
390pub(crate) const GIT_REF_THRESHOLD: usize = 1024;
391#[cfg(feature = "internal")]
393pub const GIT_REF_THRESHOLD: usize = 1024;
394
395pub(crate) const STRING_VALUE_BLOB: &str = "__value";
397
398pub(crate) const LIST_VALUE_DIR: &str = "__list";
400
401pub(crate) const SET_VALUE_DIR: &str = "__set";
403
404pub(crate) const TOMBSTONE_ROOT: &str = "__tombstones";
406
407pub(crate) const TOMBSTONE_BLOB: &str = "__deleted";
409
410pub(crate) const PATH_TARGET_SEPARATOR: &str = "__target__";
412
413pub(crate) fn decode_path_target_segments(segments: &[&str]) -> Result<String> {
415 if segments.is_empty() {
416 return Err(Error::InvalidTreePath(
417 "path target must include at least one segment".into(),
418 ));
419 }
420
421 let decoded = segments
422 .iter()
423 .map(|segment| {
424 if let Some(rest) = segment.strip_prefix('~') {
425 rest.to_string()
426 } else {
427 (*segment).to_string()
428 }
429 })
430 .collect::<Vec<_>>()
431 .join("/");
432
433 Ok(decoded)
434}
435
436pub(crate) fn set_member_id(value: &str) -> String {
438 let header = format!("blob {}\0", value.len());
439 let mut hasher = Sha1::new();
440 hasher.update(header.as_bytes());
441 hasher.update(value.as_bytes());
442 format!("{:x}", hasher.finalize())
443}
444
445fn validate_key_segment(segment: &str) -> Result<()> {
446 if segment.is_empty() {
447 return Err(Error::InvalidKey("key segments cannot be empty".into()));
448 }
449 if segment == "." || segment == ".." {
450 return Err(Error::InvalidKey(format!(
451 "key segment '{segment}' is not allowed"
452 )));
453 }
454 if segment.contains('/') {
455 return Err(Error::InvalidKey(format!(
456 "key segment '{segment}' must not contain '/'"
457 )));
458 }
459 if segment.contains('\0') {
460 return Err(Error::InvalidKey(format!(
461 "key segment '{segment}' must not contain null byte"
462 )));
463 }
464 if segment.starts_with("__")
465 || segment == STRING_VALUE_BLOB
466 || segment == LIST_VALUE_DIR
467 || segment == SET_VALUE_DIR
468 {
469 return Err(Error::InvalidKey(format!(
470 "key segment '{segment}' is reserved"
471 )));
472 }
473 Ok(())
474}
475
476#[cfg(not(feature = "internal"))]
482pub(crate) fn validate_key(key: &str) -> Result<()> {
483 validate_key_inner(key)
484}
485
486#[cfg(feature = "internal")]
492pub fn validate_key(key: &str) -> Result<()> {
493 validate_key_inner(key)
494}
495
496fn validate_key_inner(key: &str) -> Result<()> {
497 if key.is_empty() {
498 return Err(Error::InvalidKey("key cannot be empty".into()));
499 }
500 for segment in key.split(':') {
501 validate_key_segment(segment)?;
502 }
503 Ok(())
504}
505
506pub(crate) fn decode_key_path_segments(segments: &[&str]) -> Result<String> {
508 if segments.is_empty() {
509 return Err(Error::InvalidKey(
510 "key path must include at least one key segment".into(),
511 ));
512 }
513 let mut decoded = Vec::with_capacity(segments.len());
514 for segment in segments {
515 validate_key_segment(segment)?;
516 decoded.push((*segment).to_string());
517 }
518 Ok(decoded.join(":"))
519}
520
521#[cfg(test)]
522#[allow(clippy::unwrap_used, clippy::expect_used)]
523mod tests {
524 use super::*;
525
526 #[test]
527 fn test_parse_commit_target() {
528 let t = Target::parse("commit:abc123").unwrap();
529 assert_eq!(t.target_type(), &TargetType::Commit);
530 assert_eq!(t.value(), Some("abc123"));
531 }
532
533 #[test]
534 fn test_parse_project_target() {
535 let t = Target::parse("project").unwrap();
536 assert_eq!(t.target_type(), &TargetType::Project);
537 assert_eq!(t.value(), None);
538 }
539
540 #[test]
541 fn test_parse_path_target_with_colon_in_value() {
542 let t = Target::parse("path:src/foo.rs").unwrap();
544 assert_eq!(t.target_type(), &TargetType::Path);
545 assert_eq!(t.value(), Some("src/foo.rs"));
546 }
547
548 #[test]
549 fn test_parse_short_value_rejected() {
550 let result = Target::parse("commit:ab");
551 assert!(result.is_err());
552 }
553
554 #[test]
555 fn test_parse_unknown_type_rejected() {
556 let result = Target::parse("unknown:abc123");
557 assert!(result.is_err());
558 }
559
560 #[test]
561 fn test_value_type_roundtrip() {
562 assert_eq!("string".parse::<ValueType>().unwrap(), ValueType::String);
563 assert_eq!("list".parse::<ValueType>().unwrap(), ValueType::List);
564 assert_eq!("set".parse::<ValueType>().unwrap(), ValueType::Set);
565 assert!("hash".parse::<ValueType>().is_err());
566 }
567
568 #[test]
569 fn test_parse_branch_target() {
570 let t = Target::parse("branch:sc-branch-1-deadbeef").unwrap();
571 assert_eq!(t.target_type(), &TargetType::Branch);
572 assert_eq!(t.value(), Some("sc-branch-1-deadbeef"));
573 }
574
575 #[test]
576 fn test_decode_path_target_segments() {
577 let decoded =
578 super::decode_path_target_segments(&["src", "~__generated", "file.rs"]).unwrap();
579 assert_eq!(decoded, "src/__generated/file.rs");
580 }
581
582 #[test]
583 fn test_decode_key_path_segments() {
584 let decoded = super::decode_key_path_segments(&["agent", "model"]).unwrap();
585 assert_eq!(decoded, "agent:model");
586 }
587
588 #[test]
589 fn test_validate_key_rejects_reserved_segments() {
590 assert!(super::validate_key("agent:__value").is_err());
591 assert!(super::validate_key("__list:chat").is_err());
592 assert!(super::validate_key("__custom:model").is_err());
593 }
594
595 #[test]
596 fn test_validate_key_rejects_unsafe_segments() {
597 assert!(super::validate_key("agent:/model").is_err());
598 assert!(super::validate_key("agent::model").is_err());
599 assert!(super::validate_key("agent:.").is_err());
600 assert!(super::validate_key("agent:..").is_err());
601 }
602
603 #[test]
604 fn test_validate_key_accepts_normal_segments() {
605 assert!(super::validate_key("agent:model:version").is_ok());
606 }
607
608 #[test]
609 fn test_meta_value_string_type() {
610 let v = MetaValue::String("hello".to_string());
611 assert_eq!(v.value_type(), ValueType::String);
612 }
613
614 #[test]
615 fn test_meta_value_list_type() {
616 let v = MetaValue::List(vec![crate::list_value::ListEntry {
617 value: "item".to_string(),
618 timestamp: 1000,
619 }]);
620 assert_eq!(v.value_type(), ValueType::List);
621 }
622
623 #[test]
624 fn test_meta_value_set_type() {
625 let mut s = std::collections::BTreeSet::new();
626 s.insert("a".to_string());
627 s.insert("b".to_string());
628 let v = MetaValue::Set(s);
629 assert_eq!(v.value_type(), ValueType::Set);
630 }
631
632 #[test]
633 fn test_meta_value_empty_list_type() {
634 let v = MetaValue::List(vec![]);
635 assert_eq!(v.value_type(), ValueType::List);
636 }
637
638 #[test]
639 fn test_meta_value_empty_set_type() {
640 let v = MetaValue::Set(std::collections::BTreeSet::new());
641 assert_eq!(v.value_type(), ValueType::Set);
642 }
643
644 #[test]
645 fn test_meta_value_clone_eq() {
646 let v1 = MetaValue::String("test".to_string());
647 let v2 = v1.clone();
648 assert_eq!(v1, v2);
649 }
650
651 #[test]
652 fn test_target_commit_constructor() {
653 let t = Target::commit("abc123").unwrap();
654 assert_eq!(t.target_type(), &TargetType::Commit);
655 assert_eq!(t.value(), Some("abc123"));
656 }
657
658 #[test]
659 fn test_target_commit_constructor_short_sha_rejected() {
660 let result = Target::commit("ab");
661 assert!(result.is_err());
662 }
663
664 #[test]
665 fn test_target_project_constructor() {
666 let t = Target::project();
667 assert_eq!(t.target_type(), &TargetType::Project);
668 assert_eq!(t.value(), None);
669 }
670
671 #[test]
672 fn test_target_path_constructor() {
673 let t = Target::path("src/main.rs");
674 assert_eq!(t.target_type(), &TargetType::Path);
675 assert_eq!(t.value(), Some("src/main.rs"));
676 }
677
678 #[test]
679 fn test_target_branch_constructor() {
680 let t = Target::branch("feature-x");
681 assert_eq!(t.target_type(), &TargetType::Branch);
682 assert_eq!(t.value(), Some("feature-x"));
683 }
684
685 #[test]
686 fn test_target_change_id_constructor() {
687 let t = Target::change_id("jj-change-abc");
688 assert_eq!(t.target_type(), &TargetType::ChangeId);
689 assert_eq!(t.value(), Some("jj-change-abc"));
690 }
691
692 #[test]
693 fn test_named_constructors_match_parse() {
694 let from_parse = Target::parse("commit:abc123").unwrap();
696 let from_ctor = Target::commit("abc123").unwrap();
697 assert_eq!(from_parse, from_ctor);
698
699 let from_parse = Target::parse("project").unwrap();
700 let from_ctor = Target::project();
701 assert_eq!(from_parse, from_ctor);
702
703 let from_parse = Target::parse("path:src/main.rs").unwrap();
704 let from_ctor = Target::path("src/main.rs");
705 assert_eq!(from_parse, from_ctor);
706
707 let from_parse = Target::parse("branch:feature-x").unwrap();
708 let from_ctor = Target::branch("feature-x");
709 assert_eq!(from_parse, from_ctor);
710
711 let from_parse = Target::parse("change-id:jj-change-abc").unwrap();
712 let from_ctor = Target::change_id("jj-change-abc");
713 assert_eq!(from_parse, from_ctor);
714 }
715}