From f888798d1c93e95e5316de6c46781c4f08402840 Mon Sep 17 00:00:00 2001 From: Noah Mayr Date: Wed, 1 May 2024 21:49:26 +0200 Subject: [PATCH] lib: add utility functions for working with topics to view and repo --- lib/src/repo.rs | 85 ++++++++++++++++++++- lib/src/view.rs | 193 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+), 1 deletion(-) diff --git a/lib/src/repo.rs b/lib/src/repo.rs index 6e01b528b8..1b3e197e4d 100644 --- a/lib/src/repo.rs +++ b/lib/src/repo.rs @@ -57,9 +57,10 @@ use crate::signing::{SignInitError, Signer}; use crate::simple_op_heads_store::SimpleOpHeadsStore; use crate::simple_op_store::SimpleOpStore; use crate::store::Store; +use crate::str_util::StringPattern; use crate::submodule_store::SubmoduleStore; use crate::transaction::Transaction; -use crate::view::View; +use crate::view::{View, ViewTopicsDiff}; use crate::{backend, dag_walk, op_store, revset}; pub trait Repo { @@ -1522,6 +1523,88 @@ impl MutableRepo { view.set_tag_target(name, new_target); } + pub fn remove_topics(&mut self, topics: Vec) -> ViewTopicsDiff { + self.update_topics_by_patterns(topics, |_, _| Some(HashSet::new())) + } + + /// Sets topic to contain to the given commit ids. + /// If empty the topic will be removed. + pub fn set_topic_commits( + &mut self, + topics: HashSet, + commit_ids: &HashSet, + exclusive_topics: bool, + exclusive_commits: bool, + ) -> ViewTopicsDiff { + let mut stats = self.update_topics(&topics, |_, old| { + Some(if exclusive_commits { + commit_ids.clone() + } else { + HashSet::from_iter(commit_ids.union(old).cloned()) + }) + }); + if exclusive_topics { + let topics = self + .view() + .topics() + .keys() + .filter(|topic| !topics.contains(*topic)) + .cloned() + .collect_vec(); + stats += self.update_topics(topics, |_, old| { + Some(HashSet::from_iter(old.difference(commit_ids).cloned())) + }); + } + stats + } + + /// Dissssociates the given topics from the given commit ids. + /// Empty topics will be removed. + pub fn disassociate_topics_from_commits( + &mut self, + topics: Vec, + commit_ids: &HashSet, + ) -> ViewTopicsDiff { + self.update_topics_by_patterns(topics, |_, old| { + Some(HashSet::from_iter(old.difference(commit_ids).cloned())) + }) + } + + fn update_topics_by_patterns( + &mut self, + topics: Vec, + update: F, + ) -> ViewTopicsDiff + where + F: Fn(&str, &HashSet) -> Option>, + { + self.update_topics( + self.view() + .topics() + .keys() + .filter(|topic| topics.iter().any(|pattern| pattern.matches(topic))) + .cloned() + .collect_vec(), + update, + ) + } + + pub fn update_topics(&mut self, topics: I, update: F) -> ViewTopicsDiff + where + S: AsRef + ToString, + I: IntoIterator, + F: Fn(&str, &HashSet) -> Option>, + { + self.view_mut().update_topics(topics, update) + } + + pub fn update_existing_topics(&mut self, update: F) -> ViewTopicsDiff + where + F: Fn(&str, &HashSet) -> Option>, + { + self.view_mut().update_existing_topics(update) + } + pub fn get_git_ref(&self, name: &str) -> RefTarget { self.view.with_ref(|v| v.get_git_ref(name).clone()) } diff --git a/lib/src/view.rs b/lib/src/view.rs index 573c5ec279..ffc7b8dc64 100644 --- a/lib/src/view.rs +++ b/lib/src/view.rs @@ -14,7 +14,9 @@ #![allow(missing_docs)] +use std::collections::hash_map::IntoIter; use std::collections::{BTreeMap, HashMap, HashSet}; +use std::ops::{AddAssign, Deref, DerefMut}; use itertools::Itertools; @@ -303,6 +305,108 @@ impl View { } } + #[must_use] + pub fn has_topic(&self, name: &str) -> bool { + self.data.topics.contains_key(name) + } + + pub fn remove_topic(&mut self, name: &str) { + self.data.topics.remove(name); + } + + /// Iterates local topics `(name, commit_ids)`s in lexicographical order. + pub fn topics(&self) -> &BTreeMap> { + &self.data.topics + } + + /// Iterates topics `(name, commit_ids)`s containing the given commit id in + /// lexicographical order + pub fn topics_containing_commit<'a: 'b, 'b>( + &'a self, + commit_id: &'b CommitId, + ) -> impl Iterator + 'b { + self.topics().iter().filter_map(|(topic, commit_ids)| { + if commit_ids.contains(commit_id) { + Some(topic) + } else { + None + } + }) + } + + /// Iterates topic `(name, commit_ids)`s matching the given pattern. + /// Entries are sorted by `name`. + pub fn topics_matching<'a: 'b, 'b>( + &'a self, + pattern: &'b StringPattern, + ) -> impl Iterator)> + 'b { + pattern.filter_btree_map(&self.data.topics) + } + + pub fn get_topic_commits(&self, name: &str) -> Option<&HashSet> { + self.data.topics.get(name) + } + + pub fn update_topics(&mut self, topics: I, update: F) -> ViewTopicsDiff + where + S: AsRef + ToString, + I: IntoIterator, + F: Fn(&str, &HashSet) -> Option>, + { + let mut stats = ViewTopicsDiff::default(); + + for topic in topics { + let Some(changes) = self.update_topic(topic.as_ref(), &update) else { + continue; + }; + + stats.insert(topic.to_string(), changes); + } + stats + } + + pub fn update_existing_topics(&mut self, update: F) -> ViewTopicsDiff + where + F: Fn(&str, &HashSet) -> Option>, + { + let mut stats = ViewTopicsDiff::default(); + + for topic in self.data.topics.keys().cloned().collect_vec() { + let Some(changes) = self.update_topic(&topic, &update) else { + continue; + }; + + stats.insert(topic.to_string(), changes); + } + stats + } + + fn update_topic(&mut self, name: S, update: &F) -> Option + where + S: AsRef + ToString, + F: Fn(&str, &HashSet) -> Option>, + { + let before = self.data.topics.get(name.as_ref()); + let after = update(name.as_ref(), before.unwrap_or(&HashSet::new()))?; + let changes = TopicDiff::new(before.unwrap_or(&Default::default()), &after); + if changes.is_empty() { + return None; + } + + self.set_topic_commits(name.to_string(), after); + Some(changes) + } + + /// Sets topic to contain to the given commit ids. + /// If empty the topic will be removed. + fn set_topic_commits(&mut self, name: String, commit_ids: HashSet) { + if commit_ids.is_empty() { + self.data.topics.remove(&name); + } else { + self.data.topics.insert(name.to_string(), commit_ids); + } + } + pub fn get_git_ref(&self, name: &str) -> &RefTarget { self.data.git_refs.get(name).flatten() } @@ -379,3 +483,92 @@ impl View { &mut self.data } } + +#[derive(Clone, Debug, Default)] +pub struct ViewTopicsDiff(pub HashMap); + +impl ViewTopicsDiff { + pub fn affected(&self) -> HashSet<&CommitId> { + self.iter().fold(HashSet::new(), |mut acc, (_, diff)| { + acc.extend(&diff.added); + acc.extend(&diff.removed); + acc + }) + } +} + +impl AddAssign for ViewTopicsDiff { + fn add_assign(&mut self, rhs: Self) { + for (topic, stats) in rhs.0 { + *self.0.entry(topic).or_default() += stats; + } + } +} + +impl Deref for ViewTopicsDiff { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ViewTopicsDiff { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl IntoIterator for ViewTopicsDiff { + type Item = (String, TopicDiff); + + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +#[derive(Clone, Debug, Default)] +pub struct TopicDiff { + added: HashSet, + removed: HashSet, +} + +impl TopicDiff { + pub fn len(&self) -> usize { + self.added.len() + self.removed.len() + } + + pub fn is_empty(&self) -> bool { + self.added.is_empty() && self.removed.is_empty() + } + + pub fn new(before: &HashSet, after: &HashSet) -> Self { + Self { + added: HashSet::from_iter(after.difference(before).cloned()), + removed: HashSet::from_iter(before.difference(after).cloned()), + } + } + + pub fn added(&self) -> &HashSet { + &self.added + } + + pub fn removed(&self) -> &HashSet { + &self.removed + } +} + +impl AddAssign for TopicDiff { + fn add_assign(&mut self, rhs: Self) { + for removed in &rhs.removed { + self.added.remove(removed); + } + for added in &rhs.added { + self.removed.remove(added); + } + self.added.extend(rhs.added); + self.removed.extend(rhs.removed); + } +}