use kcode_k1_rust_package::{LibraryId, SourceFile, SourcePackage};
use kcode_k1_transaction_id::TxId;
use semver::Version;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use toml_edit::{DocumentMut, value};
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct UnpublishedId(TxId);
impl UnpublishedId {
pub const fn new(transaction: TxId) -> Self {
Self(transaction)
}
pub const fn transaction(&self) -> TxId {
self.0
}
}
impl Display for UnpublishedId {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.0, formatter)
}
}
impl FromStr for UnpublishedId {
type Err = WorktreeError;
fn from_str(source: &str) -> Result<Self, Self::Err> {
let transaction = TxId::from_str(source)
.map_err(|_| WorktreeError("invalid unpublished version".into()))?;
if transaction.to_string() != source {
return fail("invalid unpublished version");
}
Ok(Self(transaction))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SourceSelector {
Published(Version),
Unpublished(UnpublishedId),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Worktree {
id: UnpublishedId,
revision: TxId,
source: SourcePackage,
}
impl Worktree {
pub fn new(id: UnpublishedId, revision: TxId, source: SourcePackage) -> Self {
Self {
id,
revision,
source,
}
}
pub const fn id(&self) -> UnpublishedId {
self.id
}
pub const fn revision(&self) -> TxId {
self.revision
}
pub fn source(&self) -> &SourcePackage {
&self.source
}
pub fn replace(
&mut self,
expected_revision: TxId,
revision: TxId,
source: SourcePackage,
) -> Result<(), WorktreeError> {
if self.revision != expected_revision {
return fail("unpublished version changed");
}
if source.id() != self.source.id() {
return fail("unpublished source identity changed");
}
self.revision = revision;
self.source = source;
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OpenedSource {
selector: SourceSelector,
source: SourcePackage,
revision: Option<TxId>,
}
impl OpenedSource {
pub fn published(version: Version, source: SourcePackage) -> Self {
Self {
selector: SourceSelector::Published(version),
source,
revision: None,
}
}
pub fn unpublished(worktree: &Worktree) -> Self {
Self {
selector: SourceSelector::Unpublished(worktree.id()),
source: worktree.source().clone(),
revision: Some(worktree.revision()),
}
}
pub fn selector(&self) -> &SourceSelector {
&self.selector
}
pub fn source(&self) -> &SourcePackage {
&self.source
}
pub const fn revision(&self) -> Option<TxId> {
self.revision
}
pub fn into_source(self) -> SourcePackage {
self.source
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorktreeError(String);
impl WorktreeError {
pub fn message(&self) -> &str {
&self.0
}
}
impl Display for WorktreeError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.0)
}
}
impl std::error::Error for WorktreeError {}
pub fn publication_source(
source: &SourcePackage,
version: Version,
) -> Result<SourcePackage, WorktreeError> {
let manifest = source
.files()
.iter()
.find(|file| file.path() == "Cargo.toml")
.ok_or_else(|| WorktreeError("missing Cargo.toml".into()))?;
let manifest = std::str::from_utf8(manifest.bytes())
.map_err(|_| WorktreeError("Cargo.toml must be UTF-8".into()))?;
let mut document = manifest
.parse::<DocumentMut>()
.map_err(|_| WorktreeError("Cargo.toml must be valid TOML".into()))?;
let package = document
.get_mut("package")
.and_then(|item| item.as_table_mut())
.ok_or_else(|| WorktreeError("missing [package]".into()))?;
if !package.contains_key("version") {
return fail("missing package version");
}
package["version"] = value(version.to_string());
let mut files = source.files().to_vec();
let target = files
.iter_mut()
.find(|file| file.path() == "Cargo.toml")
.expect("manifest was found");
*target = SourceFile::new("Cargo.toml", document.to_string().into_bytes());
let identity = LibraryId::new(source.id().family().clone(), version)
.map_err(|error| WorktreeError(error.to_string()))?;
SourcePackage::new(identity, files).map_err(|error| WorktreeError(error.to_string()))
}
fn fail<T>(message: impl Into<String>) -> Result<T, WorktreeError> {
Err(WorktreeError(message.into()))
}
#[cfg(test)]
mod tests {
use super::*;
use kcode_k1_rust_package::{AuthorityId, LibraryFamily};
fn source() -> SourcePackage {
let family =
LibraryFamily::new(AuthorityId::new(TxId::from_bytes([1; 12])), "demo").unwrap();
let identity = LibraryId::new(family, Version::new(0, 0, 0)).unwrap();
let manifest = r#"[package]
name = "k1-010101010101010101010101-demo"
version = "0.0.0"
edition = "2024"
autobins = false
autoexamples = false
autotests = false
autobenches = false
[lib]
name = "demo"
path = "src/lib.rs"
[workspace]
resolver = "3"
"#;
SourcePackage::new(
identity,
vec![
SourceFile::new("Cargo.toml", manifest.as_bytes().to_vec()),
SourceFile::new("Documentation.md", b"docs\n".to_vec()),
SourceFile::new("src/lib.rs", b"pub fn run() {}\n".to_vec()),
],
)
.unwrap()
}
#[test]
fn unpublished_ids_are_canonical_transaction_ids() {
let id = UnpublishedId::new(TxId::from_bytes([7; 12]));
assert_eq!(id.to_string().len(), 24);
assert_eq!(id.to_string().parse::<UnpublishedId>().unwrap(), id);
assert!("ABCDEFABCDEFABCDEFABCDEF".parse::<UnpublishedId>().is_err());
}
#[test]
fn publication_rewrites_only_manifest_version_semantics() {
let original = source();
let published = publication_source(&original, Version::new(1, 2, 3)).unwrap();
assert_eq!(published.id().version(), &Version::new(1, 2, 3));
assert!(
std::str::from_utf8(published.files()[0].bytes())
.unwrap()
.contains("version = \"1.2.3\"")
);
assert_eq!(original.id().version(), &Version::new(0, 0, 0));
assert_eq!(published.files()[1..], original.files()[1..]);
}
}