Skip to content

feat: add .tangram/tags directory and update tg tag logic #588

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/client/src/tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ impl Tag {
}
self.as_str()
}

pub fn ancestors(&self) -> impl Iterator<Item = Self> + 'static {
let components = self.components.clone();
(1..components.len()).map(move |n| tg::Tag::with_components(components[0..n].to_vec()))
}
}

impl AsRef<str> for Tag {
Expand Down Expand Up @@ -257,4 +262,13 @@ mod tests {
.is_err()
);
}

#[test]
fn ancestors() {
let tag = "foo".parse::<tg::Tag>().unwrap();
assert!(tag.ancestors().next().is_none());

let tag = "foo/bar".parse::<tg::Tag>().unwrap();
assert_eq!(tag.ancestors().next(), Some("foo".parse().unwrap()));
}
}
112 changes: 111 additions & 1 deletion packages/server/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,81 @@ impl Compiler {
return Ok(module);
}

// Handle a path in the tags directory.
if let Ok(path) = path.strip_prefix(self.server.tags_path()) {
// Check if the path is empty.
if path.as_os_str().is_empty() {
return Err(tg::error!(%uri, "invalid uri"));
}

// Look up the tag.
let mut pattern = Vec::new();
let mut components = path.components();
let mut output = None;
while let Some(component) = components.next() {
let component = component
.as_os_str()
.to_str()
.ok_or_else(|| tg::error!("invalid tag component"))?;
pattern.push(component.parse()?);
let pattern = tg::tag::Pattern::with_components(pattern.clone());
output = self.server.try_get_tag(&pattern).await?;
if output.is_some() {
break;
}
}
let output = output.ok_or_else(|| tg::error!(%uri, "could not resolve tag"))?;
let item = output
.item
.right()
.ok_or_else(|| tg::error!(%uri, "expected an object"))?;
let path: PathBuf = components.collect();

// Infer the kind.
let kind = if path.extension().is_some_and(|extension| extension == "js") {
tg::module::Kind::Js
} else if path.extension().is_some_and(|extension| extension == "ts") {
tg::module::Kind::Ts
} else if item.is_directory() {
tg::module::Kind::Directory
} else if item.is_file() {
tg::module::Kind::File
} else if item.is_symlink() {
tg::module::Kind::Symlink
} else {
return Err(tg::error!("expected a directory, file, or symlink"));
};

// Materialize the symlink if it does not exist.
let link = self.server.tags_path().join(output.tag.to_string());
if !matches!(tokio::fs::try_exists(&link).await, Ok(true)) {
// Create the parent directory.
tokio::fs::create_dir_all(link.parent().unwrap())
.await
.map_err(|source| tg::error!(!source, "failed to create the tag directory"))?;

// Get the symlink target.
let artifact = self.server.artifacts_path().join(item.to_string());
let target = crate::util::path::diff(link.parent().unwrap(), &artifact)?;

// Create the symlink.
tokio::fs::symlink(target, link)
.await
.map_err(|source| tg::error!(!source, "failed to get the symlink"))?;
}

// Create the referent.
let path = (!path.as_os_str().is_empty()).then_some(path);
let referent = tg::Referent {
item: tg::module::data::Item::Object(item),
path,
tag: Some(output.tag),
};

// Return the module.
return Ok(tg::module::Data { kind, referent });
}

// Handle a path in the cache directory.
if let Ok(path) = path.strip_prefix(self.server.cache_path()) {
#[allow(clippy::case_sensitive_file_extension_comparisons)]
Expand Down Expand Up @@ -810,7 +885,6 @@ impl Compiler {

// Create the module.
let module = self.server.module_for_path(path).await?;

Ok(module)
}

Expand Down Expand Up @@ -857,6 +931,42 @@ impl Compiler {
Ok(uri)
},

tg::module::Data {
referent:
tg::Referent {
item: tg::module::data::Item::Object(object),
tag: Some(tag),
path: subpath,
},
..
} => {
let artifact = object
.clone()
.try_into()
.map_err(|_| tg::error!("expected an artifact"))?;
if self.server.vfs.lock().unwrap().is_none() {
let arg = tg::checkout::Arg {
artifact,
dependencies: false,
force: false,
lockfile: false,
path: None,
};
self.server
.checkout(arg)
.await?
.map_ok(|_| ())
.try_collect::<()>()
.await?;
}
let path = self.server.tags_path().join(tag.to_string());
let path = subpath
.as_ref()
.map_or_else(|| path.clone(), |p| path.join(p));
let uri = format!("file://{}", path.display()).parse().unwrap();
Ok(uri)
},

tg::module::Data {
referent:
tg::Referent {
Expand Down
5 changes: 5 additions & 0 deletions packages/server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,11 @@ impl Server {
self.path.join("logs")
}

#[must_use]
pub fn tags_path(&self) -> PathBuf {
self.path.join("tags")
}

#[must_use]
pub fn temp_path(&self) -> PathBuf {
self.path.join("tmp")
Expand Down
83 changes: 51 additions & 32 deletions packages/server/src/tag/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ use tangram_database::{self as db, prelude::*};
use tangram_either::Either;
use tangram_http::{Body, request::Ext as _, response::builder::Ext as _};

#[derive(Clone, Debug, serde::Deserialize)]
struct Row {
tag: tg::Tag,
item: Either<tg::process::Id, tg::object::Id>,
}

impl Server {
pub async fn list_tags(
&self,
Expand Down Expand Up @@ -53,26 +59,60 @@ impl Server {
}

async fn list_tags_local(&self, arg: tg::tag::list::Arg) -> tg::Result<tg::tag::list::Output> {
let mut rows = self.query_tags(&arg.pattern).await?;

// If there is no match and the last component is normal, search for any versioned items.
if matches!(
arg.pattern.components().last(),
Some(tg::tag::pattern::Component::Normal(_))
) && rows.is_empty()
{
let mut pattern = arg.pattern.into_components();
pattern.push(tg::tag::pattern::Component::Wildcard);
rows = self
.query_tags(&tg::tag::Pattern::with_components(pattern))
.await?;
}

// Reverse if requested.
if arg.reverse {
rows.reverse();
}

// Limit.
if let Some(length) = arg.length {
rows.truncate(length.to_usize().unwrap());
}

// Create the output.
let data = rows
.into_iter()
.map(|row| tg::tag::get::Output {
tag: row.tag,
item: row.item,
remote: None,
})
.collect();
let output = tg::tag::list::Output { data };

Ok(output)
}

async fn query_tags(&self, pattern: &tg::tag::Pattern) -> tg::Result<Vec<Row>> {
// Get a database connection.
let connection = self
.database
.connection()
.await
.map_err(|source| tg::error!(!source, "failed to get a database connection"))?;

#[derive(Clone, Debug, serde::Deserialize)]
struct Row {
tag: tg::Tag,
item: Either<tg::process::Id, tg::object::Id>,
}
let p = connection.p();
let prefix = arg
.pattern
let prefix = pattern
.as_str()
.char_indices()
.find(|(_, c)| !(c.is_alphanumeric() || matches!(c, '.' | '_' | '+' | '-' | '/')))
.map_or(arg.pattern.as_str().len(), |(i, _)| i);
let prefix = &arg.pattern.as_str()[..prefix];
.map_or(pattern.as_str().len(), |(i, _)| i);
let prefix = &pattern.as_str()[..prefix];
let statement = formatdoc!(
"
select tag, item
Expand All @@ -87,33 +127,12 @@ impl Server {
.map_err(|source| tg::error!(!source, "failed to execute the statement"))?;

// Filter the rows.
rows.retain(|row| arg.pattern.matches(&row.tag));
rows.retain(|row| pattern.matches(&row.tag));

// Sort the rows.
rows.sort_by(|a, b| a.tag.cmp(&b.tag));

// Reverse if requested.
if arg.reverse {
rows.reverse();
}

// Limit.
if let Some(length) = arg.length {
rows.truncate(length.to_usize().unwrap());
}

// Create the output.
let data = rows
.into_iter()
.map(|row| tg::tag::get::Output {
tag: row.tag,
item: row.item,
remote: None,
})
.collect();
let output = tg::tag::list::Output { data };

Ok(output)
Ok(rows)
}

pub(crate) async fn handle_list_tags_request<H>(
Expand Down
60 changes: 60 additions & 0 deletions packages/server/src/tag/put.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ use tangram_messenger::prelude::*;

impl Server {
pub async fn put_tag(&self, tag: &tg::Tag, mut arg: tg::tag::put::Arg) -> tg::Result<()> {
if tag.is_empty() {
return Err(tg::error!("invalid tag"));
}

// If the remote arg is set, then forward the request.
if let Some(remote) = arg.remote.take() {
let remote = self.get_remote_client(remote).await?;
Expand All @@ -17,6 +21,9 @@ impl Server {
remote.put_tag(tag, arg).await?;
}

// Check if there are any ancestors of this tag.
self.check_tag_ancestors(tag).await?;

// Get a database connection.
let connection = self
.database
Expand All @@ -39,6 +46,16 @@ impl Server {
.await
.map_err(|source| tg::error!(!source, "failed to execute the statement"))?;

// Create the tag entry.
if let Some(artifact) = arg
.item
.as_ref()
.right()
.and_then(|id| id.clone().try_into().ok())
{
self.create_tag_dir_entry(tag, &artifact).await?;
}

// Send the tag message.
let message = crate::index::Message::PutTag(crate::index::PutTagMessage {
tag: tag.to_string(),
Expand All @@ -55,6 +72,49 @@ impl Server {
Ok(())
}

async fn check_tag_ancestors(&self, tag: &tg::Tag) -> tg::Result<()> {
let connection = self
.database
.connection()
.await
.map_err(|source| tg::error!(!source, "failed to get a database connection"))?;
let p = connection.p();
let statement = formatdoc!(
"
select count(*) from tags where tag = {p}1;
"
);
for ancestor in tag.ancestors() {
let params = db::params![ancestor.to_string()];
let count = connection
.query_one_value_into::<u64>(statement.clone().into(), params)
.await
.map_err(|source| tg::error!(!source, "failed to perform the query"))?;
if count != 0 {
return Err(tg::error!("there is an existing tag `{ancestor}`"));
}
}
Ok(())
}

async fn create_tag_dir_entry(
&self,
tag: &tg::Tag,
artifact: &tg::artifact::Id,
) -> tg::Result<()> {
let link = self.tags_path().join(tag.to_string());
tokio::fs::create_dir_all(link.parent().unwrap())
.await
.map_err(|source| tg::error!(!source, "failed to create the tag directory"))?;
crate::util::fs::remove(&link).await.ok();
let target = self.artifacts_path().join(artifact.to_string());
let path = crate::util::path::diff(link.parent().unwrap(), &target)?;
tokio::fs::symlink(path, &link)
.await
.map_err(|source| tg::error!(!source, "failed to create the tag entry"))?;
Ok(())
}

pub(crate) async fn handle_put_tag_request<H>(
handle: &H,
request: http::Request<Body>,
Expand Down