Skip to main content

kcode_k1_rust_worktree/
lib.rs

1use kcode_k1_rust_package::{LibraryId, SourceFile, SourcePackage};
2use kcode_k1_transaction_id::TxId;
3use semver::Version;
4use std::fmt::{Display, Formatter};
5use std::str::FromStr;
6use toml_edit::{DocumentMut, value};
7
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub struct UnpublishedId(TxId);
10
11impl UnpublishedId {
12    pub const fn new(transaction: TxId) -> Self {
13        Self(transaction)
14    }
15
16    pub const fn transaction(&self) -> TxId {
17        self.0
18    }
19}
20
21impl Display for UnpublishedId {
22    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
23        Display::fmt(&self.0, formatter)
24    }
25}
26
27impl FromStr for UnpublishedId {
28    type Err = WorktreeError;
29
30    fn from_str(source: &str) -> Result<Self, Self::Err> {
31        let transaction = TxId::from_str(source)
32            .map_err(|_| WorktreeError("invalid unpublished version".into()))?;
33        if transaction.to_string() != source {
34            return fail("invalid unpublished version");
35        }
36        Ok(Self(transaction))
37    }
38}
39
40#[derive(Clone, Debug, Eq, PartialEq)]
41pub enum SourceSelector {
42    Published(Version),
43    Unpublished(UnpublishedId),
44}
45
46#[derive(Clone, Debug, Eq, PartialEq)]
47pub struct Worktree {
48    id: UnpublishedId,
49    revision: TxId,
50    source: SourcePackage,
51}
52
53impl Worktree {
54    pub fn new(id: UnpublishedId, revision: TxId, source: SourcePackage) -> Self {
55        Self {
56            id,
57            revision,
58            source,
59        }
60    }
61
62    pub const fn id(&self) -> UnpublishedId {
63        self.id
64    }
65
66    pub const fn revision(&self) -> TxId {
67        self.revision
68    }
69
70    pub fn source(&self) -> &SourcePackage {
71        &self.source
72    }
73
74    pub fn replace(
75        &mut self,
76        expected_revision: TxId,
77        revision: TxId,
78        source: SourcePackage,
79    ) -> Result<(), WorktreeError> {
80        if self.revision != expected_revision {
81            return fail("unpublished version changed");
82        }
83        if source.id() != self.source.id() {
84            return fail("unpublished source identity changed");
85        }
86        self.revision = revision;
87        self.source = source;
88        Ok(())
89    }
90}
91
92#[derive(Clone, Debug, Eq, PartialEq)]
93pub struct OpenedSource {
94    selector: SourceSelector,
95    source: SourcePackage,
96    revision: Option<TxId>,
97}
98
99impl OpenedSource {
100    pub fn published(version: Version, source: SourcePackage) -> Self {
101        Self {
102            selector: SourceSelector::Published(version),
103            source,
104            revision: None,
105        }
106    }
107
108    pub fn unpublished(worktree: &Worktree) -> Self {
109        Self {
110            selector: SourceSelector::Unpublished(worktree.id()),
111            source: worktree.source().clone(),
112            revision: Some(worktree.revision()),
113        }
114    }
115
116    pub fn selector(&self) -> &SourceSelector {
117        &self.selector
118    }
119
120    pub fn source(&self) -> &SourcePackage {
121        &self.source
122    }
123
124    pub const fn revision(&self) -> Option<TxId> {
125        self.revision
126    }
127
128    pub fn into_source(self) -> SourcePackage {
129        self.source
130    }
131}
132
133#[derive(Clone, Debug, Eq, PartialEq)]
134pub struct WorktreeError(String);
135
136impl WorktreeError {
137    pub fn message(&self) -> &str {
138        &self.0
139    }
140}
141
142impl Display for WorktreeError {
143    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
144        formatter.write_str(&self.0)
145    }
146}
147
148impl std::error::Error for WorktreeError {}
149
150pub fn publication_source(
151    source: &SourcePackage,
152    version: Version,
153) -> Result<SourcePackage, WorktreeError> {
154    let manifest = source
155        .files()
156        .iter()
157        .find(|file| file.path() == "Cargo.toml")
158        .ok_or_else(|| WorktreeError("missing Cargo.toml".into()))?;
159    let manifest = std::str::from_utf8(manifest.bytes())
160        .map_err(|_| WorktreeError("Cargo.toml must be UTF-8".into()))?;
161    let mut document = manifest
162        .parse::<DocumentMut>()
163        .map_err(|_| WorktreeError("Cargo.toml must be valid TOML".into()))?;
164    let package = document
165        .get_mut("package")
166        .and_then(|item| item.as_table_mut())
167        .ok_or_else(|| WorktreeError("missing [package]".into()))?;
168    if !package.contains_key("version") {
169        return fail("missing package version");
170    }
171    package["version"] = value(version.to_string());
172
173    let mut files = source.files().to_vec();
174    let target = files
175        .iter_mut()
176        .find(|file| file.path() == "Cargo.toml")
177        .expect("manifest was found");
178    *target = SourceFile::new("Cargo.toml", document.to_string().into_bytes());
179    let identity = LibraryId::new(source.id().family().clone(), version)
180        .map_err(|error| WorktreeError(error.to_string()))?;
181    SourcePackage::new(identity, files).map_err(|error| WorktreeError(error.to_string()))
182}
183
184fn fail<T>(message: impl Into<String>) -> Result<T, WorktreeError> {
185    Err(WorktreeError(message.into()))
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use kcode_k1_rust_package::{AuthorityId, LibraryFamily};
192
193    fn source() -> SourcePackage {
194        let family =
195            LibraryFamily::new(AuthorityId::new(TxId::from_bytes([1; 12])), "demo").unwrap();
196        let identity = LibraryId::new(family, Version::new(0, 0, 0)).unwrap();
197        let manifest = r#"[package]
198name = "k1-010101010101010101010101-demo"
199version = "0.0.0"
200edition = "2024"
201autobins = false
202autoexamples = false
203autotests = false
204autobenches = false
205
206[lib]
207name = "demo"
208path = "src/lib.rs"
209
210[workspace]
211resolver = "3"
212"#;
213        SourcePackage::new(
214            identity,
215            vec![
216                SourceFile::new("Cargo.toml", manifest.as_bytes().to_vec()),
217                SourceFile::new("Documentation.md", b"docs\n".to_vec()),
218                SourceFile::new("src/lib.rs", b"pub fn run() {}\n".to_vec()),
219            ],
220        )
221        .unwrap()
222    }
223
224    #[test]
225    fn unpublished_ids_are_canonical_transaction_ids() {
226        let id = UnpublishedId::new(TxId::from_bytes([7; 12]));
227        assert_eq!(id.to_string().len(), 24);
228        assert_eq!(id.to_string().parse::<UnpublishedId>().unwrap(), id);
229        assert!("ABCDEFABCDEFABCDEFABCDEF".parse::<UnpublishedId>().is_err());
230    }
231
232    #[test]
233    fn publication_rewrites_only_manifest_version_semantics() {
234        let original = source();
235        let published = publication_source(&original, Version::new(1, 2, 3)).unwrap();
236        assert_eq!(published.id().version(), &Version::new(1, 2, 3));
237        assert!(
238            std::str::from_utf8(published.files()[0].bytes())
239                .unwrap()
240                .contains("version = \"1.2.3\"")
241        );
242        assert_eq!(original.id().version(), &Version::new(0, 0, 0));
243        assert_eq!(published.files()[1..], original.files()[1..]);
244    }
245}