kcode_k1_rust_worktree/
lib.rs1use 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}