Match post tag names exactly

This commit is contained in:
2026-07-22 18:58:29 +02:00
parent a1c04ff898
commit 3051331b87

View File

@@ -78,7 +78,9 @@ pub fn update_tag(
Ok(())
}
/// Delete a tag: remove from all posts' tag arrays, delete from DB, rewrite tags.json.
/// Delete a tag: remove its exact name from posts, delete from DB, rewrite tags.json.
/// Tag entity lookup remains case-insensitive, but portable post tag arrays
/// follow bDS2 and Allium's exact string membership semantics.
pub fn delete_tag(
conn: &Connection,
data_dir: &Path,
@@ -96,7 +98,9 @@ pub fn delete_tag(
Ok(())
}
/// Rename a tag: update all posts' tag arrays, update tag in DB, rewrite tags.json.
/// Rename a tag: replace its exact name in posts, update the DB and tags.json.
/// Tag entity lookup remains case-insensitive, but portable post tag arrays
/// follow bDS2 and Allium's exact string membership semantics.
pub fn rename_tag(
conn: &Connection,
data_dir: &Path,
@@ -113,12 +117,12 @@ pub fn rename_tag(
let now = now_unix_ms();
let mut modified = Vec::new();
for mut post in posts {
if post.tags.iter().any(|t| t.eq_ignore_ascii_case(&old_name)) {
if post.tags.iter().any(|t| t == &old_name) {
post.tags = post
.tags
.into_iter()
.map(|t| {
if t.eq_ignore_ascii_case(&old_name) {
if t == old_name {
new_name.to_string()
} else {
t
@@ -142,6 +146,8 @@ pub fn rename_tag(
/// Merge multiple source tags into one target tag.
/// For each source: update posts (remove source name, add target name if not present), delete source.
/// Source and target membership in portable post tag arrays is exact, matching
/// bDS2 and Allium; tag entity lookup and uniqueness remain case-insensitive.
pub fn merge_tags(
conn: &Connection,
data_dir: &Path,
@@ -161,20 +167,12 @@ pub fn merge_tags(
let posts = post_q::list_posts_by_project(conn, project_id)?;
let now = now_unix_ms();
for mut post in posts {
let has_source = post
.tags
.iter()
.any(|t| t.eq_ignore_ascii_case(&source_tag.name));
let has_source = post.tags.iter().any(|t| t == &source_tag.name);
if has_source {
// Remove source tag name
post.tags
.retain(|t| !t.eq_ignore_ascii_case(&source_tag.name));
post.tags.retain(|t| t != &source_tag.name);
// Add target tag name if not already present
if !post
.tags
.iter()
.any(|t| t.eq_ignore_ascii_case(&target_tag.name))
{
if !post.tags.iter().any(|t| t == &target_tag.name) {
post.tags.push(target_tag.name.clone());
}
post.updated_at = now;
@@ -354,8 +352,8 @@ fn remove_tag_name_from_posts(
let now = now_unix_ms();
let mut modified = Vec::new();
for mut post in posts {
if post.tags.iter().any(|t| t.eq_ignore_ascii_case(tag_name)) {
post.tags.retain(|t| !t.eq_ignore_ascii_case(tag_name));
if post.tags.iter().any(|t| t == tag_name) {
post.tags.retain(|t| t != tag_name);
post.updated_at = now;
post_q::update_post(conn, &post)?;
modified.push(post.id.clone());
@@ -519,6 +517,57 @@ mod tests {
assert_eq!(all[0].name, "target");
}
#[test]
fn tag_mutations_match_post_tag_names_exactly() {
let (db, dir) = setup();
let renamed = create_tag(db.conn(), dir.path(), "p1", "rust", None).unwrap();
insert_post(
db.conn(),
&make_post("rename", "rename", vec!["Rust".into(), "rust".into()]),
)
.unwrap();
rename_tag(db.conn(), dir.path(), "p1", &renamed.id, "golang").unwrap();
assert_eq!(
post_q::get_post_by_id(db.conn(), "rename").unwrap().tags,
vec!["Rust", "golang"]
);
let source = create_tag(db.conn(), dir.path(), "p1", "source", None).unwrap();
let target = create_tag(db.conn(), dir.path(), "p1", "target", None).unwrap();
insert_post(
db.conn(),
&make_post(
"merge",
"merge",
vec!["SOURCE".into(), "source".into(), "Target".into()],
),
)
.unwrap();
merge_tags(db.conn(), dir.path(), "p1", &[&source.id], &target.id).unwrap();
assert_eq!(
post_q::get_post_by_id(db.conn(), "merge").unwrap().tags,
vec!["SOURCE", "Target", "target"]
);
let deleted = create_tag(db.conn(), dir.path(), "p1", "delete", None).unwrap();
insert_post(
db.conn(),
&make_post("delete", "delete", vec!["DELETE".into(), "delete".into()]),
)
.unwrap();
delete_tag(db.conn(), dir.path(), "p1", &deleted.id).unwrap();
assert_eq!(
post_q::get_post_by_id(db.conn(), "delete").unwrap().tags,
vec!["DELETE"]
);
}
#[test]
fn sync_tags_from_posts_creates_missing() {
let (db, _dir) = setup();