From f3c78e564570de1f3b69338d3457b72a12065367 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sun, 13 Mar 2022 21:59:50 +0000 Subject: [PATCH] Less allocations in the hot path --- Cargo.lock | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 5 +++++ README.md | 2 +- src/config.rs | 3 ++- src/main.rs | 26 +++++++++++++------------- src/metadata.rs | 31 ++++++++++++++++++------------- src/util.rs | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- src/protocol/high_level.rs | 13 +++++++------ src/protocol/low_level.rs | 29 +++++++++++++++++++---------- src/providers/gitlab.rs | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------- src/providers/mod.rs | 6 ++++-- 11 files changed, 249 insertions(+), 106 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3eec481..257e824 100644 --- a/Cargo.lock +++ a/Cargo.lock @@ -22,6 +22,17 @@ ] [[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] name = "aho-corasick" version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -44,6 +55,12 @@ version = "1.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" [[package]] name = "async-trait" @@ -549,8 +566,10 @@ version = "0.1.0" dependencies = [ "anyhow", + "arrayvec", "async-trait", "bytes", + "cargo-platform", "cargo_metadata", "clap", "flate2", @@ -559,10 +578,11 @@ "indexmap", "indoc", "itoa", - "parking_lot", + "parking_lot 0.12.0", "parse_link_header", "percent-encoding", "reqwest", + "semver", "serde", "serde_json", "sha1", @@ -575,6 +595,8 @@ "toml", "tracing", "tracing-subscriber", + "url", + "ustr", "uuid", ] @@ -991,6 +1013,17 @@ checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" dependencies = [ "memchr", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.5", ] [[package]] @@ -1000,7 +1033,21 @@ checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.1", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", ] [[package]] @@ -1595,7 +1642,7 @@ "mio", "num_cpus", "once_cell", - "parking_lot", + "parking_lot 0.12.0", "pin-project-lite", "signal-hook-registry", "socket2", @@ -1785,6 +1832,19 @@ "idna", "matches", "percent-encoding", + "serde", +] + +[[package]] +name = "ustr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd539d8973e229b9d04f15d36e6a8f8d8f85f946b366f06bb001aaed3fa9dd9" +dependencies = [ + "ahash", + "byteorder", + "lazy_static", + "parking_lot 0.11.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0a83231..a9eed18 100644 --- a/Cargo.toml +++ a/Cargo.toml @@ -10,9 +10,11 @@ [dependencies] anyhow = "1" +arrayvec = "0.7" async-trait = "0.1" bytes = "1.1" cargo_metadata = "0.14" +cargo-platform = "0.1" clap = { version = "3.1", features = ["derive", "cargo"] } flate2 = "1.0" futures = "0.3" @@ -24,6 +26,7 @@ parking_lot = "0.12" percent-encoding = "2.1" reqwest = { version = "0.11", features = ["json"] } +semver = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1" sha1 = "0.10" @@ -36,4 +39,6 @@ tokio = { version = "1.17", features = ["full"] } tokio-util = { version = "0.7", features = ["codec"] } toml = "0.5" +url = { version = "2.2", features = ["serde"] } +ustr = "0.8" uuid = { version = "1.0.0-alpha.1", features = ["v4"] } diff --git a/README.md b/README.md index 8a65fda..c5079bb 100644 --- a/README.md +++ a/README.md @@ -43,7 +43,7 @@ To release your package from CI, add a new pipeline step: -``` +```yaml stage: - release-crate diff --git a/src/config.rs b/src/config.rs index 35effc1..fca06c6 100644 --- a/src/config.rs +++ a/src/config.rs @@ -1,8 +1,9 @@ #![allow(clippy::module_name_repetitions)] use clap::Parser; use serde::{de::DeserializeOwned, Deserialize}; use std::path::PathBuf; +use url::Url; #[derive(Parser)] #[clap(version = clap::crate_version!(), author = clap::crate_authors!())] @@ -21,7 +22,7 @@ #[derive(Deserialize)] #[serde(rename_all = "kebab-case")] pub struct GitlabConfig { - pub uri: String, + pub uri: Url, pub admin_token: String, } diff --git a/src/main.rs b/src/main.rs index 772f5e3..e2689d2 100644 --- a/src/main.rs +++ a/src/main.rs @@ -17,7 +17,7 @@ low_level::{HashOutput, PackFileEntry}, packet_line::PktLine, }, - providers::{gitlab::Gitlab, Group, PackageProvider, Release, User, UserProvider}, + providers::{gitlab::Gitlab, Group, PackageProvider, Release, ReleaseName, User, UserProvider}, util::get_crate_folder, }; use anyhow::anyhow; @@ -147,7 +147,7 @@ pub struct Handler { codec: GitCodec, gitlab: Arc, - user: Option, + user: Option>, group: Option, // fetcher_future: Option>>>, input_bytes: BytesMut, @@ -161,7 +161,7 @@ } impl Handler { - fn user(&self) -> anyhow::Result<&User> { + fn user(&self) -> anyhow::Result<&Arc> { self.user.as_ref().ok_or(anyhow::anyhow!("no user set")) } @@ -187,7 +187,7 @@ /// and groups them by crate. async fn fetch_releases_by_crate( &self, - ) -> anyhow::Result>> { + ) -> anyhow::Result>> { let user = self.user()?; let group = self.group()?; @@ -197,7 +197,7 @@ .fetch_releases_for_group(group, user) .await? { - res.entry((path, release.name.clone())) + res.entry((path, Arc::clone(&release.name))) .or_insert_with(Vec::new) .push(release); } @@ -266,7 +266,7 @@ // return the cached value if we've generated the packfile for // this connection already if let Some(packfile_cache) = &self.packfile_cache { - return Ok(packfile_cache.clone()); + return Ok(Arc::clone(packfile_cache)); } // create the high-level packfile generator @@ -282,11 +282,11 @@ // generate the config for the user, containing the download // url template from gitlab and the impersonation token embedded let config_json = Bytes::from(serde_json::to_vec(&CargoConfig { - dl: self.gitlab.cargo_dl_uri(group, &token), + dl: self.gitlab.cargo_dl_uri(group, &token)?, })?); // write config.json to the root of the repo - packfile.insert(vec![], "config.json".to_string(), config_json)?; + packfile.insert(&[], "config.json".into(), config_json)?; // fetch the releases for every project within the given group let releases_by_crate = self.fetch_releases_by_crate().await?; @@ -316,8 +316,8 @@ // insert the crate version metadata into the packfile packfile.insert( - get_crate_folder(crate_name), - crate_name.to_string(), + &get_crate_folder(crate_name), + Arc::clone(crate_name).into(), buffer.split().freeze(), )?; } @@ -396,7 +396,7 @@ &user.username, if by_ssh_key { "SSH Key" } else { "Build Token" }, ); - self.user = Some(user); + self.user = Some(Arc::new(user)); self.finished_auth(Auth::Accept).await } else { info!("Public key rejected"); @@ -481,11 +481,11 @@ let span = info_span!(parent: &self.span, "shell_request"); Box::pin(capture_errors(async move { - let username = self.user()?.username.clone(); + let user = Arc::clone(self.user()?); write!( &mut self.output_bytes, "Hi there, {}! You've successfully authenticated, but {} does not provide shell access.\r\n", - username, + user.username, env!("CARGO_PKG_NAME") )?; info!("Shell requested, dropping connection"); diff --git a/src/metadata.rs b/src/metadata.rs index 906c503..c996ad3 100644 --- a/src/metadata.rs +++ a/src/metadata.rs @@ -1,8 +1,10 @@ #![allow(clippy::module_name_repetitions)] -use cargo_metadata::Package; +use cargo_metadata::{DependencyKind, Package}; +use cargo_platform::Platform; +use semver::{Version, VersionReq}; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::{borrow::Cow, collections::HashMap}; /// Transforms metadata from `cargo metadata` to the standard one-line JSON used in cargo registries. /// @@ -20,21 +22,22 @@ Some(CargoIndexCrateMetadata { name: package.name, - vers: package.version.to_string(), + vers: package.version, deps: package .dependencies .into_iter() .map(|v| CargoIndexCrateMetadataDependency { name: v.name, - req: v.req.to_string(), + req: v.req, features: v.features, optional: v.optional, default_features: v.uses_default_features, - target: v.target.map(|v| v.to_string()), - kind: v.kind.to_string(), - registry: Some(v.registry.unwrap_or_else(|| { - "https://github.com/rust-lang/crates.io-index.git".to_string() - })), + target: v.target, + kind: v.kind, + registry: Some(v.registry.map_or( + Cow::Borrowed("https://github.com/rust-lang/crates.io-index.git"), + Cow::Owned, + )), package: v.rename, }) .collect(), @@ -53,7 +56,7 @@ #[derive(Serialize, Deserialize, Debug)] pub struct CargoIndexCrateMetadata { name: String, - vers: String, + vers: Version, deps: Vec, cksum: String, features: HashMap>, @@ -64,12 +67,12 @@ #[derive(Serialize, Deserialize, Debug)] pub struct CargoIndexCrateMetadataDependency { name: String, - req: String, + req: VersionReq, features: Vec, optional: bool, default_features: bool, - target: Option, - kind: String, - registry: Option, + target: Option, + kind: DependencyKind, + registry: Option>, package: Option, } diff --git a/src/util.rs b/src/util.rs index 422e38d..1ec0df9 100644 --- a/src/util.rs +++ a/src/util.rs @@ -1,3 +1,12 @@ +use arrayvec::ArrayVec; +use std::{ + borrow::Cow, + fmt::{Debug, Display, Formatter}, + ops::Deref, + sync::Arc, +}; +use ustr::ustr; + #[must_use] pub fn format_fingerprint(fingerprint: &str) -> String { format!("SHA256:{}", fingerprint) @@ -5,22 +14,75 @@ /// Crates with a total of 1, 2 or 3 characters in the same are written out to directories named /// 1, 2 or 3 respectively as per the cargo spec. Anything else we'll build out a normal tree for -/// using the frist four characters of the crate name, 2 for the first directory and the other 2 +/// using the first four characters of the crate name, 2 for the first directory and the other 2 /// for the second. #[must_use] -pub fn get_crate_folder(crate_name: &str) -> Vec { - let mut folders = Vec::new(); +pub fn get_crate_folder(crate_name: &str) -> ArrayVec<&'static str, 2> { + let mut folders = ArrayVec::new(); match crate_name.len() { 0 => {} - 1 => folders.push("1".to_string()), - 2 => folders.push("2".to_string()), - 3 => folders.push("3".to_string()), + 1 => folders.push("1"), + 2 => folders.push("2"), + 3 => folders.push("3"), _ => { - folders.push(crate_name[..2].to_string()); - folders.push(crate_name[2..4].to_string()); + folders.push(ustr(&crate_name[..2]).as_str()); + folders.push(ustr(&crate_name[2..4]).as_str()); } } folders +} + +#[derive(Debug, Hash, PartialEq, Eq)] +pub enum ArcOrCowStr { + Arc(Arc), + Cow(Cow<'static, str>), +} + +impl From> for ArcOrCowStr { + fn from(v: Arc) -> Self { + Self::Arc(v) + } +} + +impl From> for ArcOrCowStr { + fn from(v: Cow<'static, str>) -> Self { + Self::Cow(v) + } +} + +impl From<&'static str> for ArcOrCowStr { + fn from(v: &'static str) -> Self { + Self::Cow(Cow::Borrowed(v)) + } +} + +impl From for ArcOrCowStr { + fn from(v: String) -> Self { + Self::Cow(Cow::Owned(v)) + } +} + +impl AsRef for ArcOrCowStr { + fn as_ref(&self) -> &str { + match self { + Self::Arc(v) => v.as_ref(), + Self::Cow(v) => v.as_ref(), + } + } +} + +impl Deref for ArcOrCowStr { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +impl Display for ArcOrCowStr { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&**self, f) + } } diff --git a/src/protocol/high_level.rs b/src/protocol/high_level.rs index e7dae0d..58ab82b 100644 --- a/src/protocol/high_level.rs +++ a/src/protocol/high_level.rs @@ -6,6 +6,7 @@ //! for our purposes because `cargo` will `git pull --force` from our Git //! server, allowing us to ignore any history the client may have. +use crate::util::ArcOrCowStr; use bytes::Bytes; use indexmap::IndexMap; @@ -34,8 +35,8 @@ /// and a `file` of `"my-file"`. pub fn insert( &mut self, - path: Vec, - file: String, + path: &[&'static str], + file: ArcOrCowStr, content: Bytes, ) -> Result<(), anyhow::Error> { // we'll initialise the directory to the root of the tree, this means @@ -48,7 +49,7 @@ for part in path { let tree_item = directory .0 - .entry(part) + .entry((*part).into()) .or_insert_with(|| Box::new(TreeItem::Tree(Tree::default()))); if let TreeItem::Tree(d) = tree_item.as_mut() { @@ -98,7 +99,7 @@ let commit = PackFileEntry::Commit(Commit { tree: tree_hash, - author: commit_user.clone(), + author: commit_user, committer: commit_user, message, }); @@ -116,7 +117,7 @@ /// An in-progress tree builder, containing file hashes along with their names or nested trees #[derive(Default, Debug)] -struct Tree(IndexMap>); +struct Tree(IndexMap>); impl Tree { /// Recursively writes the the whole tree out to the given `pack_file`, @@ -132,7 +133,7 @@ tree.push(match *item { TreeItem::Blob(hash) => LowLevelTreeItem { kind: TreeItemKind::File, - sort_name: name.clone(), + sort_name: name.to_string(), name, hash, }, diff --git a/src/protocol/low_level.rs b/src/protocol/low_level.rs index b013cb8..4f19623 100644 --- a/src/protocol/low_level.rs +++ a/src/protocol/low_level.rs @@ -1,7 +1,12 @@ +use crate::util::ArcOrCowStr; use bytes::{BufMut, Bytes, BytesMut}; use flate2::{write::ZlibEncoder, Compression}; use sha1::Digest; -use std::{convert::TryInto, fmt::Write, io::Write as IoWrite}; +use std::{ + convert::TryInto, + fmt::{Display, Formatter, Write}, + io::Write as IoWrite, +}; pub type HashOutput = [u8; 20]; @@ -73,8 +78,8 @@ out.extend_from_slice(&tree_hex); out.write_char('\n')?; - writeln!(out, "author {}", self.author.encode())?; - writeln!(out, "committer {}", self.committer.encode())?; + writeln!(out, "author {}", self.author)?; + writeln!(out, "committer {}", self.committer)?; write!(out, "\n{}", self.message)?; Ok(()) @@ -91,24 +96,26 @@ } } -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub struct CommitUserInfo { pub name: &'static str, pub email: &'static str, pub time: time::OffsetDateTime, } -impl CommitUserInfo { - fn encode(&self) -> String { - // TODO: remove `format!`, `format_args!`? - format!( +impl Display for CommitUserInfo { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, "{} <{}> {} +0000", self.name, self.email, self.time.unix_timestamp() ) } +} +impl CommitUserInfo { #[must_use] pub fn size(&self) -> usize { let timestamp_len = itoa::Buffer::new().format(self.time.unix_timestamp()).len(); @@ -138,10 +145,10 @@ } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct TreeItem { pub kind: TreeItemKind, - pub name: String, + pub name: ArcOrCowStr, pub hash: HashOutput, pub sort_name: String, } @@ -161,7 +168,7 @@ } } -#[derive(Debug, Clone)] // could be copy but Vec> +#[derive(Debug)] // could be copy but Vec> pub enum PackFileEntry { // jordan@Jordans-MacBook-Pro-2 0d % printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" | cat - f5/473259d9674ed66239766a013f96a3550374e3 | gzip -dc // commit 1068tree 0d586b48bc42e8591773d3d8a7223551c39d453c diff --git a/src/providers/gitlab.rs b/src/providers/gitlab.rs index 74bc949..3ed4a9c 100644 --- a/src/providers/gitlab.rs +++ a/src/providers/gitlab.rs @@ -7,13 +7,13 @@ use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use reqwest::header; use serde::{Deserialize, Serialize}; -use std::borrow::Cow; -use std::sync::Arc; +use std::{borrow::Cow, sync::Arc}; use tracing::Instrument; +use url::Url; pub struct Gitlab { client: reqwest::Client, - base_url: String, + base_url: Url, } impl Gitlab { @@ -28,7 +28,7 @@ client: reqwest::ClientBuilder::new() .default_headers(headers) .build()?, - base_url: format!("{}/api/v4", config.uri), + base_url: config.uri.join("api/v4/")?, }) } } @@ -48,7 +48,7 @@ if username == "gitlab-ci-token" { let res: GitlabJobResponse = handle_error( self.client - .get(format!("{}/job", self.base_url)) + .get(self.base_url.join("job/")?) .header("JOB-TOKEN", password) .send() .await?, @@ -67,19 +67,14 @@ } async fn find_user_by_ssh_key(&self, fingerprint: &str) -> anyhow::Result> { - let res: GitlabSshKeyLookupResponse = handle_error( - self.client - .get(format!( - "{}/keys?fingerprint={}", - self.base_url, - utf8_percent_encode(fingerprint, NON_ALPHANUMERIC) - )) - .send() - .await?, - ) - .await? - .json() - .await?; + let mut url = self.base_url.join("keys")?; + url.query_pairs_mut() + .append_pair("fingerprint", fingerprint); + + let res: GitlabSshKeyLookupResponse = handle_error(self.client.get(url).send().await?) + .await? + .json() + .await?; Ok(res.user.map(|u| User { id: u.id, username: u.username, @@ -89,10 +84,10 @@ async fn fetch_token_for_user(&self, user: &User) -> anyhow::Result { let impersonation_token: GitlabImpersonationTokenResponse = handle_error( self.client - .post(format!( - "{}/users/{}/impersonation_tokens", - self.base_url, user.id - )) + .post( + self.base_url + .join(&format!("users/{}/impersonation_tokens", user.id))?, + ) .json(&GitlabImpersonationTokenRequest { name: env!("CARGO_PKG_NAME"), scopes: vec!["api"], @@ -113,14 +108,14 @@ type CratePath = Arc; async fn fetch_group(self: Arc, group: &str, do_as: &User) -> anyhow::Result { - let uri = format!( - "{}/groups/{}?sudo={}", - self.base_url, - utf8_percent_encode(group, NON_ALPHANUMERIC), - do_as.id - ); - - let req = handle_error(self.client.get(uri).send().await?) + let mut url = self + .base_url + .join("groups/")? + .join(&utf8_percent_encode(group, NON_ALPHANUMERIC).to_string())?; + url.query_pairs_mut() + .append_pair("sudo", itoa::Buffer::new().format(do_as.id)); + + let req = handle_error(self.client.get(url).send().await?) .await? .json::() .await? @@ -134,12 +129,19 @@ group: &Group, do_as: &User, ) -> anyhow::Result> { - let mut next_uri = Some(format!( - "{}/groups/{}/packages?per_page=100&pagination=keyset&sort=asc&sudo={}", - self.base_url, - utf8_percent_encode(&group.name, NON_ALPHANUMERIC), - do_as.id - )); + let mut next_uri = Some({ + let mut uri = self + .base_url + .join(&format!("groups/{}/packages", group.id,))?; + { + let mut query = uri.query_pairs_mut(); + query.append_pair("per_page", itoa::Buffer::new().format(100u16)); + query.append_pair("pagination", "keyset"); + query.append_pair("sort", "asc"); + query.append_pair("sudo", itoa::Buffer::new().format(do_as.id)); + } + uri + }); let futures = FuturesUnordered::new(); @@ -150,14 +152,14 @@ let mut link_header = parse_link_header::parse_with_rel(link_header.to_str()?)?; if let Some(next) = link_header.remove("next") { - next_uri = Some(next.raw_uri); + next_uri = Some(next.raw_uri.parse()?); } } let res: Vec = res.json().await?; for release in res { - let this = self.clone(); + let this = Arc::clone(&self); futures.push(tokio::spawn( async move { @@ -201,7 +203,7 @@ ( Arc::clone(&package_path), Release { - name: release.name, + name: Arc::from(release.name), version: release.version, checksum: package_file.file_sha256, }, @@ -226,7 +228,7 @@ path: &Self::CratePath, version: &str, ) -> anyhow::Result { - let uri = format!("{}{}", self.base_url, path.metadata_uri(version),); + let uri = self.base_url.join(&path.metadata_uri(version))?; Ok(handle_error(self.client.get(uri).send().await?) .await? @@ -234,12 +236,12 @@ .await?) } - fn cargo_dl_uri(&self, group: &Group, token: &str) -> String { - format!( - "{}/groups/{}/packages/generic/{{sha256-checksum}}/{{crate}}-{{version}}.crate?private_token={token}", - self.base_url, - group.id, - ) + fn cargo_dl_uri(&self, group: &Group, token: &str) -> anyhow::Result { + let uri = self + .base_url + .join("groups/")? + .join(&format!("{}/", group.id))?; + Ok(format!("{uri}packages/generic/{{sha256-checksum}}/{{crate}}-{{version}}.crate?private_token={token}")) } } @@ -287,7 +289,7 @@ #[must_use] pub fn metadata_uri(&self, version: &str) -> String { format!( - "/projects/{}/packages/generic/{}/{version}/metadata.json", + "projects/{}/packages/generic/{}/{version}/metadata.json", self.project, self.package_name ) } diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 4aebf12..7ad56a1 100644 --- a/src/providers/mod.rs +++ a/src/providers/mod.rs @@ -35,7 +35,7 @@ version: &str, ) -> anyhow::Result; - fn cargo_dl_uri(&self, group: &Group, token: &str) -> String; + fn cargo_dl_uri(&self, group: &Group, token: &str) -> anyhow::Result; } #[derive(Debug, Clone)] @@ -50,9 +50,11 @@ pub name: String, } +pub type ReleaseName = Arc; + #[derive(Debug)] pub struct Release { - pub name: String, + pub name: ReleaseName, pub version: String, pub checksum: String, } -- rgit 0.1.3