From 60a089c7b91741b08391b6c3e1b36db1433af0bc Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 7 Nov 2024 19:10:54 +0100 Subject: [PATCH] feat: add `merge_base_octopus()`. --- Cargo.lock | 7 + crate-status.md | 2 + gix-revision/Cargo.toml | 1 + gix-revision/src/merge_base.rs | 251 ------------------ gix-revision/src/merge_base/function.rs | 230 ++++++++++++++++ gix-revision/src/merge_base/mod.rs | 57 ++++ .../merge_base_octopus_repos.tar | Bin 0 -> 188928 bytes .../fixtures/merge_base_octopus_repos.sh | 43 +++ gix-revision/tests/merge_base/mod.rs | 90 ------- .../tests/{ => revision}/describe/format.rs | 0 .../tests/{ => revision}/describe/mod.rs | 0 .../tests/{revision.rs => revision/main.rs} | 0 gix-revision/tests/revision/merge_base/mod.rs | 157 +++++++++++ .../tests/{ => revision}/spec/display.rs | 0 gix-revision/tests/{ => revision}/spec/mod.rs | 0 .../spec/parse/anchor/at_symbol.rs | 0 .../spec/parse/anchor/colon_symbol.rs | 0 .../spec/parse/anchor/describe.rs | 0 .../{ => revision}/spec/parse/anchor/hash.rs | 0 .../{ => revision}/spec/parse/anchor/mod.rs | 0 .../spec/parse/anchor/refnames.rs | 0 .../tests/{ => revision}/spec/parse/kind.rs | 0 .../tests/{ => revision}/spec/parse/mod.rs | 0 .../spec/parse/navigate/caret_symbol.rs | 0 .../spec/parse/navigate/colon_symbol.rs | 0 .../{ => revision}/spec/parse/navigate/mod.rs | 0 .../spec/parse/navigate/tilde_symbol.rs | 0 27 files changed, 497 insertions(+), 341 deletions(-) delete mode 100644 gix-revision/src/merge_base.rs create mode 100644 gix-revision/src/merge_base/function.rs create mode 100644 gix-revision/src/merge_base/mod.rs create mode 100644 gix-revision/tests/fixtures/generated-archives/merge_base_octopus_repos.tar create mode 100644 gix-revision/tests/fixtures/merge_base_octopus_repos.sh delete mode 100644 gix-revision/tests/merge_base/mod.rs rename gix-revision/tests/{ => revision}/describe/format.rs (100%) rename gix-revision/tests/{ => revision}/describe/mod.rs (100%) rename gix-revision/tests/{revision.rs => revision/main.rs} (100%) create mode 100644 gix-revision/tests/revision/merge_base/mod.rs rename gix-revision/tests/{ => revision}/spec/display.rs (100%) rename gix-revision/tests/{ => revision}/spec/mod.rs (100%) rename gix-revision/tests/{ => revision}/spec/parse/anchor/at_symbol.rs (100%) rename gix-revision/tests/{ => revision}/spec/parse/anchor/colon_symbol.rs (100%) rename gix-revision/tests/{ => revision}/spec/parse/anchor/describe.rs (100%) rename gix-revision/tests/{ => revision}/spec/parse/anchor/hash.rs (100%) rename gix-revision/tests/{ => revision}/spec/parse/anchor/mod.rs (100%) rename gix-revision/tests/{ => revision}/spec/parse/anchor/refnames.rs (100%) rename gix-revision/tests/{ => revision}/spec/parse/kind.rs (100%) rename gix-revision/tests/{ => revision}/spec/parse/mod.rs (100%) rename gix-revision/tests/{ => revision}/spec/parse/navigate/caret_symbol.rs (100%) rename gix-revision/tests/{ => revision}/spec/parse/navigate/colon_symbol.rs (100%) rename gix-revision/tests/{ => revision}/spec/parse/navigate/mod.rs (100%) rename gix-revision/tests/{ => revision}/spec/parse/navigate/tilde_symbol.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 083a1ea1c78..a7b7c19c122 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2465,6 +2465,7 @@ dependencies = [ "gix-revwalk 0.16.0", "gix-testtools", "gix-trace 0.1.11", + "permutohedron", "serde", "thiserror", ] @@ -3780,6 +3781,12 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "permutohedron" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b687ff7b5da449d39e418ad391e5e08da53ec334903ddbb921db208908fc372c" + [[package]] name = "pin-project" version = "1.1.5" diff --git a/crate-status.md b/crate-status.md index 24f179e2e8e..b0316d7bf5c 100644 --- a/crate-status.md +++ b/crate-status.md @@ -621,6 +621,8 @@ Make it the best-performing implementation and the most convenient one. ### gix-revision * [x] `describe()` (similar to `git name-rev`) +* [x] merge-base +* [x] merge-base octopus * parse specifications * [x] parsing and navigation * [x] revision ranges diff --git a/gix-revision/Cargo.toml b/gix-revision/Cargo.toml index dd869916b10..54003abfbb3 100644 --- a/gix-revision/Cargo.toml +++ b/gix-revision/Cargo.toml @@ -44,6 +44,7 @@ document-features = { version = "0.2.1", optional = true } [dev-dependencies] gix-odb = { path = "../gix-odb" } gix-testtools = { path = "../tests/tools" } +permutohedron = "0.2.4" [package.metadata.docs.rs] all-features = true diff --git a/gix-revision/src/merge_base.rs b/gix-revision/src/merge_base.rs deleted file mode 100644 index 39eab9c97dd..00000000000 --- a/gix-revision/src/merge_base.rs +++ /dev/null @@ -1,251 +0,0 @@ -bitflags::bitflags! { - /// The flags used in the graph for finding [merge bases](crate::merge_base()). - #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] - pub struct Flags: u8 { - /// The commit belongs to the graph reachable by the first commit - const COMMIT1 = 1 << 0; - /// The commit belongs to the graph reachable by all other commits. - const COMMIT2 = 1 << 1; - - /// Marks the commit as done, it's reachable by both COMMIT1 and COMMIT2. - const STALE = 1 << 2; - /// The commit was already put ontto the results list. - const RESULT = 1 << 3; - } -} - -/// The error returned by the [`merge_base()`][function::merge_base()] function. -#[derive(Debug, thiserror::Error)] -#[allow(missing_docs)] -pub enum Error { - #[error("A commit could not be inserted into the graph")] - InsertCommit(#[from] gix_revwalk::graph::get_or_insert_default::Error), -} - -pub(crate) mod function { - use super::Error; - use crate::{merge_base::Flags, Graph, PriorityQueue}; - use gix_hash::ObjectId; - use gix_revwalk::graph; - use std::cmp::Ordering; - - /// Given a commit at `first` id, traverse the commit `graph` and return all possible merge-base between it and `others`, - /// sorted from best to worst. Returns `None` if there is no merge-base as `first` and `others` don't share history. - /// If `others` is empty, `Some(first)` is returned. - /// - /// Note that this function doesn't do any work if `first` is contained in `others`, which is when `first` will be returned - /// as only merge-base right away. This is even the case if some commits of `others` are disjoint. - /// - /// # Performance - /// - /// For repeated calls, be sure to re-use `graph` as its content will be kept and reused for a great speed-up. The contained flags - /// will automatically be cleared. - pub fn merge_base( - first: ObjectId, - others: &[ObjectId], - graph: &mut Graph<'_, '_, graph::Commit>, - ) -> Result>, Error> { - let _span = gix_trace::coarse!("gix_revision::merge_base()", ?first, ?others); - if others.is_empty() || others.contains(&first) { - return Ok(Some(vec![first])); - } - - graph.clear_commit_data(|f| *f = Flags::empty()); - let bases = paint_down_to_common(first, others, graph)?; - - let bases = remove_redundant(&bases, graph)?; - Ok((!bases.is_empty()).then_some(bases)) - } - - /// Remove all those commits from `commits` if they are in the history of another commit in `commits`. - /// That way, we return only the topologically most recent commits in `commits`. - fn remove_redundant( - commits: &[(ObjectId, GenThenTime)], - graph: &mut Graph<'_, '_, graph::Commit>, - ) -> Result, Error> { - if commits.is_empty() { - return Ok(Vec::new()); - } - graph.clear_commit_data(|f| *f = Flags::empty()); - let _span = gix_trace::detail!("gix_revision::remove_redundant()", num_commits = %commits.len()); - let sorted_commits = { - let mut v = commits.to_vec(); - v.sort_by(|a, b| a.1.cmp(&b.1)); - v - }; - let mut min_gen_pos = 0; - let mut min_gen = sorted_commits[min_gen_pos].1.generation; - - let mut walk_start = Vec::with_capacity(commits.len()); - for (id, _) in commits { - let commit = graph.get_mut(id).expect("previously added"); - commit.data |= Flags::RESULT; - for parent_id in commit.parents.clone() { - graph.get_or_insert_full_commit(parent_id, |parent| { - // prevent double-addition - if !parent.data.contains(Flags::STALE) { - parent.data |= Flags::STALE; - walk_start.push((parent_id, GenThenTime::from(&*parent))); - } - })?; - } - } - walk_start.sort_by(|a, b| a.0.cmp(&b.0)); - // allow walking everything at first. - walk_start - .iter_mut() - .for_each(|(id, _)| graph.get_mut(id).expect("added previously").data.remove(Flags::STALE)); - let mut count_still_independent = commits.len(); - - let mut stack = Vec::new(); - while let Some((commit_id, commit_info)) = walk_start.pop().filter(|_| count_still_independent > 1) { - stack.clear(); - graph.get_mut(&commit_id).expect("added").data |= Flags::STALE; - stack.push((commit_id, commit_info)); - - while let Some((commit_id, commit_info)) = stack.last().copied() { - let commit = graph.get_mut(&commit_id).expect("all commits have been added"); - let commit_parents = commit.parents.clone(); - if commit.data.contains(Flags::RESULT) { - commit.data.remove(Flags::RESULT); - count_still_independent -= 1; - if count_still_independent <= 1 { - break; - } - if *commit_id == *sorted_commits[min_gen_pos].0 { - while min_gen_pos < commits.len() - 1 - && graph - .get(&sorted_commits[min_gen_pos].0) - .expect("already added") - .data - .contains(Flags::STALE) - { - min_gen_pos += 1; - } - min_gen = sorted_commits[min_gen_pos].1.generation; - } - } - - if commit_info.generation < min_gen { - stack.pop(); - continue; - } - - let previous_len = stack.len(); - for parent_id in &commit_parents { - if graph - .get_or_insert_full_commit(*parent_id, |parent| { - if !parent.data.contains(Flags::STALE) { - parent.data |= Flags::STALE; - stack.push((*parent_id, GenThenTime::from(&*parent))); - } - })? - .is_some() - { - break; - } - } - - if previous_len == stack.len() { - stack.pop(); - } - } - } - - Ok(commits - .iter() - .filter_map(|(id, _info)| { - graph - .get(id) - .filter(|commit| !commit.data.contains(Flags::STALE)) - .map(|_| *id) - }) - .collect()) - } - - fn paint_down_to_common( - first: ObjectId, - others: &[ObjectId], - graph: &mut Graph<'_, '_, graph::Commit>, - ) -> Result, Error> { - let mut queue = PriorityQueue::::new(); - graph.get_or_insert_full_commit(first, |commit| { - commit.data |= Flags::COMMIT1; - queue.insert(GenThenTime::from(&*commit), first); - })?; - - for other in others { - graph.get_or_insert_full_commit(*other, |commit| { - commit.data |= Flags::COMMIT2; - queue.insert(GenThenTime::from(&*commit), *other); - })?; - } - - let mut out = Vec::new(); - while queue.iter_unordered().any(|id| { - graph - .get(id) - .map_or(false, |commit| !commit.data.contains(Flags::STALE)) - }) { - let (info, commit_id) = queue.pop().expect("we have non-stale"); - let commit = graph.get_mut(&commit_id).expect("everything queued is in graph"); - let mut flags_without_result = commit.data & (Flags::COMMIT1 | Flags::COMMIT2 | Flags::STALE); - if flags_without_result == (Flags::COMMIT1 | Flags::COMMIT2) { - if !commit.data.contains(Flags::RESULT) { - commit.data |= Flags::RESULT; - out.push((commit_id, info)); - } - flags_without_result |= Flags::STALE; - } - - for parent_id in commit.parents.clone() { - graph.get_or_insert_full_commit(parent_id, |parent| { - if (parent.data & flags_without_result) != flags_without_result { - parent.data |= flags_without_result; - queue.insert(GenThenTime::from(&*parent), parent_id); - } - })?; - } - } - - Ok(out) - } - - // TODO(ST): Should this type be used for `describe` as well? - #[derive(Debug, Clone, Copy)] - struct GenThenTime { - /// Note that the special [`GENERATION_NUMBER_INFINITY`](gix_commitgraph::GENERATION_NUMBER_INFINITY) is used to indicate - /// that no commitgraph is available. - generation: gix_revwalk::graph::Generation, - time: gix_date::SecondsSinceUnixEpoch, - } - - impl From<&graph::Commit> for GenThenTime { - fn from(commit: &graph::Commit) -> Self { - GenThenTime { - generation: commit.generation.unwrap_or(gix_commitgraph::GENERATION_NUMBER_INFINITY), - time: commit.commit_time, - } - } - } - - impl Eq for GenThenTime {} - - impl PartialEq for GenThenTime { - fn eq(&self, other: &Self) -> bool { - self.cmp(other).is_eq() - } - } - - impl PartialOrd for GenThenTime { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } - } - - impl Ord for GenThenTime { - fn cmp(&self, other: &Self) -> Ordering { - self.generation.cmp(&other.generation).then(self.time.cmp(&other.time)) - } - } -} diff --git a/gix-revision/src/merge_base/function.rs b/gix-revision/src/merge_base/function.rs new file mode 100644 index 00000000000..95903efad46 --- /dev/null +++ b/gix-revision/src/merge_base/function.rs @@ -0,0 +1,230 @@ +use super::Error; +use crate::{merge_base::Flags, Graph, PriorityQueue}; +use gix_hash::ObjectId; +use gix_revwalk::graph; +use std::cmp::Ordering; + +/// Given a commit at `first` id, traverse the commit `graph` and return all possible merge-base between it and `others`, +/// sorted from best to worst. Returns `None` if there is no merge-base as `first` and `others` don't share history. +/// If `others` is empty, `Some(first)` is returned. +/// +/// Note that this function doesn't do any work if `first` is contained in `others`, which is when `first` will be returned +/// as only merge-base right away. This is even the case if some commits of `others` are disjoint. +/// +/// Additionally, this function isn't stable and results may differ dependeing on the order in which `first` and `others` are +/// provided due to its special rules. +/// +/// If a stable result is needed, use [`merge_base::octopus()`](crate::merge_base::octopus()). +/// +/// # Performance +/// +/// For repeated calls, be sure to re-use `graph` as its content will be kept and reused for a great speed-up. The contained flags +/// will automatically be cleared. +pub fn merge_base( + first: ObjectId, + others: &[ObjectId], + graph: &mut Graph<'_, '_, graph::Commit>, +) -> Result>, Error> { + let _span = gix_trace::coarse!("gix_revision::merge_base()", ?first, ?others); + if others.is_empty() || others.contains(&first) { + return Ok(Some(vec![first])); + } + + graph.clear_commit_data(|f| *f = Flags::empty()); + let bases = paint_down_to_common(first, others, graph)?; + + let bases = remove_redundant(&bases, graph)?; + Ok((!bases.is_empty()).then_some(bases)) +} + +/// Remove all those commits from `commits` if they are in the history of another commit in `commits`. +/// That way, we return only the topologically most recent commits in `commits`. +fn remove_redundant( + commits: &[(ObjectId, GenThenTime)], + graph: &mut Graph<'_, '_, graph::Commit>, +) -> Result, Error> { + if commits.is_empty() { + return Ok(Vec::new()); + } + graph.clear_commit_data(|f| *f = Flags::empty()); + let _span = gix_trace::detail!("gix_revision::remove_redundant()", num_commits = %commits.len()); + let sorted_commits = { + let mut v = commits.to_vec(); + v.sort_by(|a, b| a.1.cmp(&b.1)); + v + }; + let mut min_gen_pos = 0; + let mut min_gen = sorted_commits[min_gen_pos].1.generation; + + let mut walk_start = Vec::with_capacity(commits.len()); + for (id, _) in commits { + let commit = graph.get_mut(id).expect("previously added"); + commit.data |= Flags::RESULT; + for parent_id in commit.parents.clone() { + graph.get_or_insert_full_commit(parent_id, |parent| { + // prevent double-addition + if !parent.data.contains(Flags::STALE) { + parent.data |= Flags::STALE; + walk_start.push((parent_id, GenThenTime::from(&*parent))); + } + })?; + } + } + walk_start.sort_by(|a, b| a.0.cmp(&b.0)); + // allow walking everything at first. + walk_start + .iter_mut() + .for_each(|(id, _)| graph.get_mut(id).expect("added previously").data.remove(Flags::STALE)); + let mut count_still_independent = commits.len(); + + let mut stack = Vec::new(); + while let Some((commit_id, commit_info)) = walk_start.pop().filter(|_| count_still_independent > 1) { + stack.clear(); + graph.get_mut(&commit_id).expect("added").data |= Flags::STALE; + stack.push((commit_id, commit_info)); + + while let Some((commit_id, commit_info)) = stack.last().copied() { + let commit = graph.get_mut(&commit_id).expect("all commits have been added"); + let commit_parents = commit.parents.clone(); + if commit.data.contains(Flags::RESULT) { + commit.data.remove(Flags::RESULT); + count_still_independent -= 1; + if count_still_independent <= 1 { + break; + } + if *commit_id == *sorted_commits[min_gen_pos].0 { + while min_gen_pos < commits.len() - 1 + && graph + .get(&sorted_commits[min_gen_pos].0) + .expect("already added") + .data + .contains(Flags::STALE) + { + min_gen_pos += 1; + } + min_gen = sorted_commits[min_gen_pos].1.generation; + } + } + + if commit_info.generation < min_gen { + stack.pop(); + continue; + } + + let previous_len = stack.len(); + for parent_id in &commit_parents { + if graph + .get_or_insert_full_commit(*parent_id, |parent| { + if !parent.data.contains(Flags::STALE) { + parent.data |= Flags::STALE; + stack.push((*parent_id, GenThenTime::from(&*parent))); + } + })? + .is_some() + { + break; + } + } + + if previous_len == stack.len() { + stack.pop(); + } + } + } + + Ok(commits + .iter() + .filter_map(|(id, _info)| { + graph + .get(id) + .filter(|commit| !commit.data.contains(Flags::STALE)) + .map(|_| *id) + }) + .collect()) +} + +fn paint_down_to_common( + first: ObjectId, + others: &[ObjectId], + graph: &mut Graph<'_, '_, graph::Commit>, +) -> Result, Error> { + let mut queue = PriorityQueue::::new(); + graph.get_or_insert_full_commit(first, |commit| { + commit.data |= Flags::COMMIT1; + queue.insert(GenThenTime::from(&*commit), first); + })?; + + for other in others { + graph.get_or_insert_full_commit(*other, |commit| { + commit.data |= Flags::COMMIT2; + queue.insert(GenThenTime::from(&*commit), *other); + })?; + } + + let mut out = Vec::new(); + while queue.iter_unordered().any(|id| { + graph + .get(id) + .map_or(false, |commit| !commit.data.contains(Flags::STALE)) + }) { + let (info, commit_id) = queue.pop().expect("we have non-stale"); + let commit = graph.get_mut(&commit_id).expect("everything queued is in graph"); + let mut flags_without_result = commit.data & (Flags::COMMIT1 | Flags::COMMIT2 | Flags::STALE); + if flags_without_result == (Flags::COMMIT1 | Flags::COMMIT2) { + if !commit.data.contains(Flags::RESULT) { + commit.data |= Flags::RESULT; + out.push((commit_id, info)); + } + flags_without_result |= Flags::STALE; + } + + for parent_id in commit.parents.clone() { + graph.get_or_insert_full_commit(parent_id, |parent| { + if (parent.data & flags_without_result) != flags_without_result { + parent.data |= flags_without_result; + queue.insert(GenThenTime::from(&*parent), parent_id); + } + })?; + } + } + + Ok(out) +} + +// TODO(ST): Should this type be used for `describe` as well? +#[derive(Debug, Clone, Copy)] +struct GenThenTime { + /// Note that the special [`GENERATION_NUMBER_INFINITY`](gix_commitgraph::GENERATION_NUMBER_INFINITY) is used to indicate + /// that no commitgraph is available. + generation: gix_revwalk::graph::Generation, + time: gix_date::SecondsSinceUnixEpoch, +} + +impl From<&graph::Commit> for GenThenTime { + fn from(commit: &graph::Commit) -> Self { + GenThenTime { + generation: commit.generation.unwrap_or(gix_commitgraph::GENERATION_NUMBER_INFINITY), + time: commit.commit_time, + } + } +} + +impl Eq for GenThenTime {} + +impl PartialEq for GenThenTime { + fn eq(&self, other: &Self) -> bool { + self.cmp(other).is_eq() + } +} + +impl PartialOrd for GenThenTime { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for GenThenTime { + fn cmp(&self, other: &Self) -> Ordering { + self.generation.cmp(&other.generation).then(self.time.cmp(&other.time)) + } +} diff --git a/gix-revision/src/merge_base/mod.rs b/gix-revision/src/merge_base/mod.rs new file mode 100644 index 00000000000..d6e00e49915 --- /dev/null +++ b/gix-revision/src/merge_base/mod.rs @@ -0,0 +1,57 @@ +bitflags::bitflags! { + /// The flags used in the graph for finding [merge bases](crate::merge_base()). + #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] + pub struct Flags: u8 { + /// The commit belongs to the graph reachable by the first commit + const COMMIT1 = 1 << 0; + /// The commit belongs to the graph reachable by all other commits. + const COMMIT2 = 1 << 1; + + /// Marks the commit as done, it's reachable by both COMMIT1 and COMMIT2. + const STALE = 1 << 2; + /// The commit was already put ontto the results list. + const RESULT = 1 << 3; + } +} + +/// The error returned by the [`merge_base()`][function::merge_base()] function. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("A commit could not be inserted into the graph")] + InsertCommit(#[from] gix_revwalk::graph::get_or_insert_default::Error), +} + +pub(crate) mod function; + +mod octopus { + use crate::merge_base::{Error, Flags}; + use gix_hash::ObjectId; + use gix_revwalk::{graph, Graph}; + + /// Given a commit at `first` id, traverse the commit `graph` and return *the best common ancestor* between it and `others`, + /// sorted from best to worst. Returns `None` if there is no common merge-base as `first` and `others` don't *all* share history. + /// If `others` is empty, `Some(first)` is returned. + /// + /// # Performance + /// + /// For repeated calls, be sure to re-use `graph` as its content will be kept and reused for a great speed-up. The contained flags + /// will automatically be cleared. + pub fn octopus( + mut first: ObjectId, + others: &[ObjectId], + graph: &mut Graph<'_, '_, graph::Commit>, + ) -> Result, Error> { + for other in others { + if let Some(next) = + crate::merge_base(first, std::slice::from_ref(other), graph)?.and_then(|bases| bases.into_iter().next()) + { + first = next; + } else { + return Ok(None); + } + } + Ok(Some(first)) + } +} +pub use octopus::octopus; diff --git a/gix-revision/tests/fixtures/generated-archives/merge_base_octopus_repos.tar b/gix-revision/tests/fixtures/generated-archives/merge_base_octopus_repos.tar new file mode 100644 index 0000000000000000000000000000000000000000..a7e3aed260eae8baaeb965e62689474fcb4588ed GIT binary patch literal 188928 zcmeFa3z%HlRURrE^XM`GJDAvp?>c9W%G2GNu6K8JtEFy@)RJb}GkPGkEE`GFI#uUX zca5s6YU)&})z)Y(7(>ka_2mY~#t?@P%>4)-CdStgk~naEAqHb0HV#}~zF-4R9NRI8 zFOSRp|F!lxb*j3$tNJC+c&a~pRCQ|aeb!!kt+m%$d+oK7sY3_;m&V`p)YKvVi@)xF z1d5OBya4HqA zk|@^J*2+zPf8R~i>}>rrxygKPxc>XQ3_VBdz5c1>O1asy{caxaV*Mv`*~#Jh?*`dB z?6b}Krxq@pJGZ#BeCG7x(z(lz>~P>nbFP1WahF97bOMv{pL;Zg$JJkR03r+P~|5Vdi*((w3V*T^k zY-Xtc-v6H&BOr_`W$OEnEX%CM%eY= z?)ZOt$p5>>_~0FjQ|W1UYBH0907w^eS#QeqGM+P;%cXN^Z^oOP&g2Wl{E+_#N9l8_ z<$(OZ=9H_up4Tq;e>yvz8S?+GF+O<5T+zvSIoHi(r)H)~PNCqHa+!=fm7R2^ys63j z^i29_dMZ0J-2Vs1>2s;&fc$@QFLT-j|3m*D`hWL=^FjMg7QKAFkjv%WQfX>(y5xF= ze9@iBd+EtyJ~LgMagQFI!B0c}9~7g{t#;e-|7xvvYmcrUK?eDMr~co`q5tQ;5K`au zPt{xgYNA<76j#0Cty-&@^qsYO#oGaNeb#>}mz(Z7|AY5;a_Ilr0g89s^zZ|zLb;mq zS7Wim*2${n-I3L`$orOGY?SLwt68&Jb=PTnmSYJOcy0oOU^NjsX4R^AifAE=bdro6 z#xP5(WnTxtzvb%f+AYtu3L9*dXm~}he47zx#am;Uach0mt6J#Y@SI}P=c~{PV{kOf zsk-LPJ{pv~hF2|mWBxb>MISz^*Zgv`*4UVkwlY|A)sx;Bq}eEYGNu8aKNT8IwYX|E zSDmH>@QSSlMr~F$f|*&I8DEUX7{b7tbiL}vg0?-SBNS`ZQhBA-aGGV{;I`8!JB5nJ zF@29XLuY;MV%2ZLiLSUgwPCql$!S%Z6PDisAdXHI-7v$J4@{Lye7Ch;YuqYVR}{+z zAWR%b@))_MZ5rNM4a?HTgSG1TfF3U?mkhqFQlqveBZhOu0A?L!wi@(zOxL1NYZ5!# zz291Mn#ENN!e(YrO9z?Yvkl8>cxLjzV@=yj>#Da7RC94L(Pp{co{FwZvXgkww}6p| z6@0AS_8N_{>&Zw4ix{Bbtva{MwN~R8h;H3OBL%F6mIbd;TL*s16j^InXpjSdHl5pD zF$@uBF&MN-tk-MhYSSv!8kku1wpT5queDYKTFXUQ@qpAlrF26lI=te-S}y~Y>#JoU zH$Yvk2kT~#M&Om0u>31{BUW2yEbQja#&o2)c!un`*tCicSrq&tV>?#0R!zLYYt#sM zvn8~yWF*rRCK?M^%&mDoM)7MCWVkg1KB2pr)P}Sd>asBB?)4u}#@w=Jjg4D(VQq5gT6#P9}Db5MF3-Xac$B+=93OAy&cgzV9^1U`};K`LTO^Ww;vOi%}E| zVxl1^S@l4_@*@_`FE>C9oGwU|!h>V=L9Ozk&=6aL2t#ruV?_}fL8h+oS)juRMImlo zZcfoBQEga_R#oI5B$%5Jk(g*%i8brdGbc|2MHyRW1gyFw%$nt`wJHRjGPC4?kuU?5 zMH~r|jUYBi#y$uYvn^7Kf^bExKVTbK$WJAmTCteRZ|5 zk7J2Ic{UgD`fhc(2i0o)(}(;S$E6x>Sl$kO0DQr!ZkM2=#XJDO_y?! zWC!;08Wd4FFHSllvkG2w-7||DyfjG)TS7Z!YOst0j%;{|S|d@xl?n+M82Zkw? zfo@2eN+saC1+2SE5+h^FD^(V^1%dnm9E|!RrJ{kfn$a8U=wg-Y;25aeU~Hq+?rE4>H|L@R)-S&r*K6To8% zL)e zG?ggf2afVotBkF=_Zy-^Uo~5Ah-ukYTiC~)6)cJoWcntf?g%wDYONKpbA$kErCCvl zEQ{yp%h;Wlp&U_T0im3*Yzhh6nzhcHySViFWd>j^Uw-stX8F-GuOo_t8J(s~z)E;3 zQ?m0Z*Snpnwkj0^G{>IfP%*FKd$AGu9v@?Gym2*AD?AA)VkL~qmPq(bER%IS9A~@- z(#(p4NGm!(v)dDUalR10^v7W@42Go8Ew${pZhv-k+=W`J;)(<@N?nw2NvV|Xf>zFH z*Lv~urDK2lPtm+{tFMj0KK9=`tqs5e_V-&>tLjv- zc|(1HGKIh}-JhUUc+}Lm2Q2U&kyv{+#ZPqKvyIZ)SZRqSK8s&VRVpF=s*_37z}E5s z46J4(bfv1;RL#JkoIn@pAx|jQ$?>+5sIA-Tg4ksMB_V(KAbzYQ0IRJ>CV(F-wKfX1 zTE(M6=fOOHPKiwh`&Rcj|##DEV=4v6WcoR6M@C` zDqhnA8v8&Z?YM~w#y5k(m&g#i0YXfgjs8G5d^@!I4XP(<2LT682`g_EA-Jh-B_lHz zfItA-&RsRTL8l++fgmH*lG-b=TNKc$7wEl#Kv8p>z5bG-HNG=ERZ!kH9j(=d2wAo9O49O~sfUFFP*0d*1UCG=YtPAnfdXcrP`E6lQYvWn6RxxP3NV( z+uwDNP19`o(As9VdF$qv2J1i%Ppn&T%&BIj{vY7Ixl#A#C_|#m9T6YnI)bvh63jqD zL_3Vfx=FR)5Z&!Npa3uT>o2noMU?3sN{fMdhpop+phiiDNl>lPju+n|E!%pdM)j1B zFf4sN&~!&LPmQ)o7m8}w-X4z?9hk;;JkrsoJ!W62a&vGa(%3#?J#E?OVo3hTkt5bc zeP0v?OSTf|$||T~cw#T;B#V(_$6}uE6a!vcpgY{++S9fMKg5;%$kJ4JlF20hp|1tD zBpj!5vzFVHA{CE7(8(Ab4hEYYy80#^9TRCbtkG-f#8D?vx-n{vrbow>%Wd;++nNJf zIAVke6s%>TQhl|t5~4pCk`7IcW-uMK3=F~p4x9*HD5DDQNAR03!Q!bS6FOHQeFQ$x zYNA7Dz(aqv)@Ux9!RF$3&8NeM+hg2AGZA2Ryqo=O>jBKN*=Tu@NsdI;cL~;X_gg2d zc*k3@EWQ{KM*7lL7a`c*L(TF>N4^5G8>L#qy%18#!I~s|3u__%1S&*H2B1H6tYn}`L{}$5Z`xxgOx7D-)wsIx4G&9E&PGC*Lr!XU z=rP63{h}nR9dWu8r?(xmxD&O-y9jFfjDs zZ1ejua)19%9llulxOeXXq8_wO{6}^wm*3?7nMx1izwXESxBWlRVz+kx;4}*RD4ZjD zQsorn{9n#0^)MH1k51PQhT|``@e!yiT8#i`=-ApJiRAEFPcA2LR6-vK6sd@qgGP*J zuvm34Sjm>P$fIvaL}RW}3c+i`qt?yGq1Y@%j0PS91MB7}1)N>cZ6(7Hwo;8&IVCVt zjVsDu>&*MQhBH zqp&~0BEt171=BQEj^GbJVUKVEuUhCG@t$fq6?KU%R(b5BM`C8_Fh|wWz!BavyBY3_H$cP}UTGe$Lbc59anK|#{Yk~uh<9Z^9{RZ~Eey){Wd^YQ@ zKbSB4rM)T|VFYhrZJmK6>#9uSNEF9df>~g`4L#2g2~wp72NlnnR|LF9lZKhBsC{+u zqHT@wq=1k@FJHlFl8;kuxL1fo^{%uU6?Lub)hLVU@hFv3Jr||1V?JS~hVtd1zJD1{ zV>lY6&C~R7FmJ~NV|*Sl>SBRPeAs#v=!f@Hbr$FWv#@aG>?blrd+d%ef; zDm*yEL*x(En95~kqJrwtFa4t*6yFj@x)T9j~7YxjV#GkXo#hAx@x9Gz|- znvQB>)3n0+nIZ6luDr3CX-QO|{acBuMb5;+)~(Se(1Gp)0iM=>h!r$zA1ig47!uQP zw$_5{t^Gn0i~$deI3Y%(;Y$QR02NWi6_5+BKX-Ja!S;WLZao~`7I&ou^x6N@`KfgG z{-4R`hvz@{cmLo1{AU{lK=~0zLJ+qM0SIYDPls@@;w5l@s1gWg-IJCeu_fjKQl2Ne z!bc*lyJMR4kfH4RElL-M$^hO0-wGn`5J3@$e27k-Hz3qO!k}=-UYBh_!80!%_(+Lb zZ+j**-v&Up~*CwBq{-+9~g zMh+7m;hBq)Aa>sdbP>c0mTCvKDIO@spIw6w!2i1PMuADJ`71jW1AX{EJB?hgo%uhr z*-7Mt8Rq{xcv{&h|I^W;s%+HX(qre>MrP}0FNxbSE;EhXqBD^mKKu%?VMaCT{Dg7A zLDImcVE;%6K;U`PF6e7I;h^0|gmTe~s26xvZ#58tBUf|ir*I)m+S6$FmLPz@!Dx&V z#uRVmU1a!mdmQ@9;9G0J4%)HA+r(iMd>!|ws>MnR2*CSz=gQni@OfJXzRGd~76n|4 z;#ayYt%y+juyqk}ZJ4+O=tW^Q_(F_=jlkqeJwTv=9E19>Zr}$gw_gnT-fGg3^j^{7 z&Vm)=?G;966l*xI5<4-Gz?%Uh{6q5Hhr?42rUF_DwjR6?ZeaK-4c-`~&XA1Dh&1O_ zHbjVn#^~Urqp<^U3D3u64?yKNgW!A+ka(h(=!VAs8rT>S^@MA1wHi?7dJdM)Bu_@- zs`!h10WD zIeruXaCeiG0JT*=SSQiu$DQ1E>I-r?|6Bdc&Kv?9&(@hU|TVP#JjAQ2b z>kS}QVpM-e6NS{iUbU^Hb(1@`A&@X|ypvLHqUQ(|bwHb<0UQ@Xe&``g0^Y(& zB%nDmt{;MEoXZzZ8D}6a;YsY7PHpMTrRBxbXU;DnN<*TGs@ABVdgA(6^2q%4@#K+s zs)GP#t?QXo9U(P`B}75)r$Fmz;&u%$7BqN)pj=G=EkzF>%~RkQ@3(uQTB3r~B489= z2foQBAT-j|cF2w36o`62F|j+L#4tjsummt%|_%97pE;IQ8p4g>w=f5~5~Re8xivCab^goN*O z8`xg{XEHeK*<}As<%jm){h$iS|ETU6i&4h=NSsh?9+S`OPNRx*7hk_EUR;=+y-@e6 z*<-X&whkDf`m};erk-`xPz{=otu~u=e>RmWL5D)b{4JK5S#1@P3$HEi{ zR(4#5s5r1p!sxhLqLFE^1nmzPr_2c;*U{lFN_`1Y-c0=l~p+dl|Mq0ZaEh zxkt-TmNwA17z<63=0>zT6nq>U`EoxW!{D|}cz4zqCyg3HK#d{Yaru!D`gGF)?P4|q zb|4I40_U$hZm=`38}6x=*VqVI9$`E(I8$>-1hl#RfJ?w$GL#S&*VDMXJNsjIkF9N3 zV{x+*h{q=8mv!d2J`K!|BYOSelb0TOt&rB0sxbq_y@<3_t(6sBeN~DtFP%Pf=@K^G zv=t26V@y~hGMb6j2d@pk{}4DVDaiw=4)mgNm2og)*;_iR_Jn1$fVxHl zKt_{d1;s{aI@xDI3V__=E=~i}hdoJ_g7q!nA#(yy69?*+xtz_ zbD(Afquiqf?bbH;ym+y9&x;q)^B!Jbad6drbva;N0TI`>t*+-BnUL4;@}TXqV3Iu; znt=${_S|(m&mD``#$^>olqf>)wR*@Ll;ToObUXhr7pA6ojIq zmw{m{E*;A*dP*N5x@F9Tu`InwE`-^P$fj^YK$>t;rPT?`p11KIA^u=~OppU-NfQ6q zgbJKou0mcR)>eAVwwk3GpbxNm*+6%_s?Z62pa~vBETahl4F&=ST)47?7l&h+ zk<1_%Jg^_re3Qmt#V1Gt*X4=+tgpg~D%ye>EXy##+5z`4N#9p3 zK^H-D18##6auE{~kT$Z+10j>g#j^*DH0U!mCBj9$HK>4jcQCv!TbCxiSocPG2;)oi zJ0qj$Q$ZGoQe>|oO=*e(YzMN(Y?ieWZON+yp%FpqrOy?|r%8;!L6&e}i!=}L1R>xX z8&%n#uoM6dL=;b+yurr$R+&=*5Mi;YL*Pe_LA#_{+<+qyiHk(~;Zh265kLmIWl7#p z#TsFo#r9&C%3j6wZ8ocvQHL-XM0tsi$+N+ntCbCLfL1pH)y5kJ^N<$>XTt!d)M@}M zN$G=49Vu;P_f@e1OICzbbXyXaAh?CxLn#HRf_`J0(K%t^q8GARc~2qrg{?wsb1Pvy zMACI{eAnd#BOO1OS3mXCH4u;D7?iZB>%lv5B~uK+X__@ zPsmIHBv+`vT$QFt^1G3%ZSkoXz_hyrrnV4%UOaE`gjOn4&0~Dm%{3Hr+!2k)OdYe1 z2jXC!8z0`V)n*aS3#6}Owdb|vW)U>qVqU?|o2MoM=c>`LXT@_)PExT5B@i(eW+NI* z28nnLUMJ*CGV1phM9acw>VQqVP-G{(?svhYDS{ zkiqw>h7V!D!uWG4>&^zV`iX|pHV2uO3bId6?Nny_^bc~cl&114%~G35z|4g&5DZAD zxnWzS3NAr{!nj|P{vfB&Lk0)|&|&4&jV+;jp)u3vscns`VayJ-4hbwN0P4<$^yiz4 z2)|&=Glxgt3iCk1{6>04Txh_aeY@=9UV&`H*fE7pJM?(hw3VpOa;iZsT&C=5v zQ!Tv9`ZJO%ZtQ&;gn&Ve)+a7dT#l~!8~&*E^wU<{JSc9g-~!925iOC9nDy0iHJ6>8 zeZ*@nth;8hI$At~0G`zK>#6H0%9VS15Am*GKy6F(!w(}-fG{qe&dwIiki?LgSF{jVf&TvAb`hTg6)_|ceulK#RCOFD`tn=S?pqa_T-fw0m2%wgx_$Z zO7infW;s!Ba@D~*mJNK0Lmo*0MWFccYt(qCPIKZag<>V zQkZShYA5B?^d%6LNVJ9YFJ+_+jV9;B&0ZX*A`P!W&MXvr1k#z7Q0b8&xQ;Adnq6Ho zceck-q0x~j2x>ZG0>kVnnz2Xj4@Z;Hh{S_Gpqe;&d0}x;#G5(~C0#i~*}@#=kcv!8 zlGMai=;QdsGUEe?fbH{M1&t-;(2Cq^t}u?Z1^c#Fi0m)oNntpW$7w)PSJ4kKw5hipaWzM};OC{2tgLIho)4SM-C80doX z03jXOQ!HEpfx&maBLlFBuEF(L1lThI2I*umI+FJ^5Mqd1xbzyp8iNPZo4}C?@=9?K z&L8s=wH(J9bPDsZ$|;m9Ws*_Nx?HOuJH2mZ(g=nPk9(VqapVZ5f0&~X6AYk)ci8}d z{T%Tv2+*zbtOLW8Xu}$xDO|P)o|JP&0S%_01j8ta^bGu~!CwoPIe%k5TOe=lB=fk% z1ImAz=VgyCET4S*@wo*~I^iPSm6dqr#&v`PK5eZRtweELT>mB^EHbgf+j;%Wg|o3U zbl=QcC!361#8NS1F>%*PG~zl&w2wq?GKaT@mxAEnwpl|kXFIJOhoDKMqO0!JkU*XX z7sx1pWe;c-UgOx3ra=&#qYcOkxOUvzWxwqVBBv>}!GVoFi~1raY0;z#WK|;Hl`Qc5 zM)2HV46>)HVB&h8jo}Vagc*4Z>1&3|fZl|IV?<35>BDQ)A{ICUW(X^q<0={7!D>3vZ6t@ULAOr*Z=qJ-Xp~o0n~0Pdx3VbPJi-6YSg&bS z%Qb5OX(tf>g&ZIdBufwxHjM)%4qPOls}ORE2}Pp;|0WSIDaa0XQxS6*gt=RutQE9; z3niY}&qY>J_Y!%-0wx%hK@+?U!h53pNTm{EP{eO4+d$|7fOeYPg~G2@$4Q^(XyFvx zIb$K2b%X<9d*$sn9-C6#)s!ympjj_W3Kd0(YmH$uR&7pVSmiMxXNRD0igrXU5=ANy zxLx`a+-ffu&C39i=q;4wCKVn)k}ImBHjP%jJI&DmGCApmTTxJF07TXubHhS(6@n(! zch==fKF%-@Z^11;1U$&NS8o+~C=5(sXX3F2_B8Wsj&F@|wRL8iTLn9-iIzRgWF%R$ zgUfX05Z8<3k(ybLsckjAi2inyTa3T4R;X3VMbKJFlDpr8r_*d~Y3mr$ym03PB@kL@ z7L68kfDmYej%n_CNs-TO(8Srw8zjy)0>FtADTdl2T$caHo#ZWFA<6(xjw3nJ&T~uG z?G-mcB=SeBALwzBwd*7lEE|snkg)(K`D^g~;}9PAVW5yStqYG)710I5_Y>U2Pcyih zM5IH4P+8Oiu=@C25QeNdf-F>2u;XT)a1O(TE3w0p>f}7sah4!5q-&am`U3p7P!|~N zMv^O`+`vYJxD?70G2nc;TwT_$bFx}Aku)cZiH?}V=KenOjtAl+=oUGs`W$5=tU zbsJs z9WO#^?WGGO2+ov1))E0C?4jc_2wqq&v$F*YG7>4f#+Y7Jg@^%+E6_Ei#3tg5ATZz= z%ri8Y^llcfJn6BQ@&yh*8MAmT%}5M64uiuE5)PS%&7^t7QQZhI3rxn+wvd)34XfO!bhI<71+u!`;`b(f(H9> z80#BsiAG@vWj?moD!gSdLwN$*xK)BP8bMt?>LOr6RcS;M?}&AQA;Mid2UQSLdBHZI zBl^aC5;;bB#UvAk%CIG(6cA2}>5Q_*Jg7~OXX=`POb1EuV3&@X$_8_yZSmev?iQiw z0Z^)D(KiGjOz_os9olPpGLyBBlCsKZZa~AjND>DK{Bh4ib38>fF`;08&uLW3l)Bi` zaJm4)R3ojU_~sild%r|G6E76Ijtqb~M$3W)z{$;E^uzzK`V!mCX#+4*rcCw(GXus? z$R3Pav;+nqvg8$Iej`wPvG(YcD0q*q3DChZ+9hQ|GsZw#ntLea_% zevSGJav3YdY6C+!n*Wd*oJ=|_-8cc4O~Ym6qpAqOYt1fCPehC>0PS5y)lE>>3z6=( z>6ygu5<7yY>h43CLk70s$5>EKAY~kZYuKP2q=bb&9J|WMeFh4KWUbW!RG$pWP|@oF z@@Dx*#6Z*O(naBvn1V(Pm}d?dp&A7kOZa*)&$%AK)G$n|X0SHQf(1wSTe%w56VhP3& zxxO`41)<&AQ^*@K@DgZP-X#twsZqj_E#%UsnzSa)MOMsIh}yc3^zOmf$a!g;X6i&^ z5s_dYG&7K{$`T|WB3*7h=&}nd%Q}NDBnur#JMbEk7o~eJi)i695jSDfr`8{o>W83E zqbnjZl#5B>L54E~HHOU|p3lIoi|0};kIaL6BT|bj44sw!rUGFA1Qz0PkBR>!enx!9 zc%``{3ECgw)qommcJ%$f;Zo_5BBb~W{@wJMt@xvZk@^3Eb&_p5JD?HULqMa@0d7huq1r|hVfN{H8M&1oRU7M4=AQFSY}ZZ zTx{k#(>K_(35t=%oEN`>T$n#p9QY6=!A>ggS&;Wo0vJ%*TLefA)(u55RSPE83xtFk zJ#egx>^Cxr1(jHJ)&fTOR>W$9pO8T{cFHttzj%oo^cpTTAR~EZoJ34ZGc@)Bd*%tn zCij*tS{Ry;7HUVVdH_MH%A&@ma5}eu$ONIcusKg*bb3)>vYUZX#3qw67$Bt~#BGU8 z$kVkz2y871V}bg&183TEuxWq)ubM8qjr}1?`ken~^6Aa@|I@?#U-yNYI{m*VLoHIB zrzXcXM8F6qMjkG~(IlQsay>U;r8!79$>t0}3Z=wAv{klH z3}KScbmt50nJf?jL8N(Cwor_s?~c9`LPLb7Y9sp@FVyBO&G^&4R6s1~l^-Bqk1{*5 zmGnwi!*w{VRis7Mu`+w$14juFB%@|-8v1kfwoWDVf6?vE^lp(PUl=Rs%Obn*6f)}# zHwCJh^i^-pyK?f%(xVr4opc2tSc#{63%(|kQUwWV;IkA5ph>ebKM%7VC6)2|CC)KO zJAsp6P$&$}7CayL;`K=G!?RlTL8}LB%;bvh!0W6 zK&dCLStB=&Wcc5dJBqSpUB1=R+rtqY$$sG@VYie2upomXU3-#6l>hn3N!bVRvHRaN zG5~bNe<33P&IE_&e|tjvK%F<2Ogk1tM1sBK;D0ni2cr}KVK74^s%Ch)KM=Z~b&a;; ze-u>k_XFTQ`X7Jp=Krbebo$WLe!RTj7|2YAtYKEaW_^F>Ebtc!x4s>o9S!=ee~Krg zdjyqu*{1%Fpsh3iXC|M`P7l|Ak9Z%X`#|fjNArUW*R#zo)_*#e$qm=PCpP!@;SSb6 zb!wNd=K61^|4mMH=l{)2Po;<=tu5Nl#|eGj4IRSS%FW($SLZ9G!8BM;$LeT_~n^0i8}_ z);je?sp9U0{}*;dS?$JLrJ^{ z#&affxpXe=&3Kd3nS7y`-wXdQJn#9RB6&BINc!52|FgMG_y5w_-1N}@vm0dZu+Jd8 z&o&3i|GLBCMw8x)8^>OI^75H4N_N}f>KuIYo$UW7cL@&ZXFLAyt^dQ7vdj1FsN3`0 z|HHKzilW75|){Et#!-TQxLYI>OeV>2QT;K?Ao&o&3i|6A<;0~`MXH`EvH z#5Eyd4~W^EMcU@ujI#}W6FdkzIk1pBF=JxBs)o;(j23`^OH?|E982!}GuWzdyW3;d;f#tVN?7c^~GWQ-@()40PCMB?r|Zwz&q6c^V$65aQzQ{`40`Y{;9!E z*SCO|>%X1;pUrje|2Po_{}1*5EfBhokMF#|`<};QFU-8;^o>uv>_5Nww|@K2-P!b; z|7h{EfBvI4-}b=iPk;W)UUuVkZ@v1m<3I7QKk|nE`gPyD{)^wf`W@euAARJ9-tmKf z_9ZhfdF*HEw_o*^$6ofpSO3`0edtgA;N6Swd-Jbuoch4ez3CPI;)lxr@gIzT^pD^B zmiK+jnb-dDZ>Dbj_CK7cz5P#K`4_i;aN!$2`3-j)-}CJ+_}r_`{-;;`U++nt`@7%$ z>`(o#pE`cw^Q{lR{ud85zwgw@K2CBt)Rq_+g#Yu|ExQl&!>;6idMN(~Lhc?L4YvNN z!J;d-fTh3u@4o+)MI={#DF3&>=YBr!E&uQQ;M22XuUPqtmmdGOFF5n^k9_=Vzx1n* z{M5Uj_=U57bfaefQ~RI)`met9$Nwz;SBJjpmoH7c?S~)v_}@SCD_`=?#N>O|*Dj?C zjkka3U5N+Ij+g%FyUxUJ->H^9ee?UT-1*!Ge)n@<{(-lSZhYI%|NftR-$&l?pWgnq zV&Tv){-c*pz5g>m_1JI!yPtddlRt9oH@@bX8?D!-n%{HkEB14e!?Cu+z##m;*K>f~ z$p7ig@c!49i?)wH46^={#l5|7H|w88{@*>Cor3}WJdghxZ2eP%MOSTs@m=iyxoMpL z4eh@z5W0_#d+-0Rdi9TdRqXw*JoTdQtv&pk-}>&2Gw=JB*WCK4-}zTBz4JS7ee;k1 z%BtUZ)A;PS&wtaG{LITfNKC_@P%k{NJ3@-~H6n zpZu-Q{dD3F|LPMT{O@mCzWT!-esS}=PJQ`4&T%-@))*Ls|55Vd*488R|7ADwpY^|n z^1nZB_u68R^`9*4?Jc`m|16vi!}Z@A-UsPD*!rhtvQusmIa!K@yjM8tr1NRq`7D%X z-2Bn(%uERh?k11IW7GSu-~OM@On06CWOGx)_}|{h+~=ovUhvTPYtCIs-#m0EHu1t2 z?DOcu;hyan7=-_GUl99mDF5#V?F_d5slo34Zvk*W`JY9uhtB*j+1w;k>1;9HzCM1Q z+yAitzWZeCKcruB2Iqf2`*Qc|{^f6vyzCp^_ceFk82{mS+ z_5QE^-oN^6dE?KLA8#IdVl=S8Q&H4YPrt-t{|Gi_r{|MJmbHTg8LhbdMUuGpI zWI{(KW>k>y%cw~-XQf$91?56ePypXp7c@3fV9BXDR)qBN4SC{_AC-xu>#XPEhs{tT z5~EA8rJyGgjUsLr@A8mUlSK-G#{EGZwm1L`DF6E?ABtdYlzuQ7VE0ul@526{OiyNp z_WuE_clVH6t$&IU|J{vu`F%I*Kb;=x|GPx@j=F8T{;7ov=guuIEuT5PxODFFBRd*6 z(zxILpT=5u>i?PP^yEiGjE0f2r!c=Xvzw|I8-)pCuuN^?&z_ z_yGnuy?AK>&0ff+OP9`^!9TA&l!ns(*c)%W|I`Pn@BPu6fBM+xKk@@_{_9UIzNqox z6L;=?+dH%G_{MLY{@};*AAQBypZE+r4*#CB7}%cw_aXT+FtZE&Zz@03|L+S;^T>S|l-;cVbar_EZ!b6>wC|wnpW1EmPXN+S|4UDG zpZ{d0^OM8)zd_ew;U%>e`oQq@S^udVay@tY z|FT)s6&RlX4~*g6H*D8`M1GvD%RlPL9@6p%4R~$>IT3Ld=y|ABji)RGODfUAfKr(+ zC>~g@-j-_dg$=e!;P}rg-}VxWDP{ribyk~}GO|vwiEKhOwgM;|O)Am`XNoXIy#~tD zdSm`La|e~tx_xM=)oW!iR!(GZ41#jz%urNAiEkcEN@+<`23^WcqQY6Rg_85EW*yAT z;>=hI7vo?E18>sxsv8U1_Ef{E7FUr;wpuE$pjd8`=cKotMp;UvV@zf(LRsm<=CzAe zzlln|iknj#DCJvnT9qd1RJSk;@*;7n=*9z7ALTEkRxPXdYB_tsvH=JacfC6p+0@Q# z;8+&(YU4o)13Oi`q>KXNg0HYnb$hNDz^tRpR?}i0)3qod0hAzW>-TG=UJOE18w8rf zx{}Zd<-Kt{tT>m7ky6=LT35YwpqevgEy;4dJr!M-q?UCIhzCoDWKe?;$x^-xh{3%1uz~iYYEMUhqC23!e&0IXyIyS=nWLK<+vz23zE=!uVEe~yTFr3pmpOc+#3 zPGUBSq8r-_O7(VT#j6A3LM8Yh2=cLgn`w0R-Ke#&E&v9i6+p5Cu~Tnacuau^!Rizd zIOJmSE2yRJC08&d3tK!Y6_X(BqH86SHtDJ5^FhPoieM-t@LOdeUO@JM(39t;zG}AK5Yw`)wy=*oD_9gG$n;I75V25Wqt;phJ4XnxRzdt&QJW|$QS<%DHZNZ8h_b>`f~rPnX>0A~5}qbD=VkDhrQQ6$XhG-U!-!c&=&olm*m z?Nqf@sTiO+_8fD731eg&q3@z zMf6UszBU5;*nha{-yQ#fk^s{~`>&0`gZtFB|0t%mHUJCQ-*2H3x>LpG4fO@uJC23a zwt!aQQB&g{uz*NV{D`ff)ROjBolK$zww4cIV3CD@kFXqgX3)_P+Pav1+mKjN<#kdfj#{s0IRJ>CV(F-wKfX1TE(M6 z=fOOHPKiwh`&Rc3pyOo+28Hot zP<^;&CEfrNrh0Q-l4P@sCU?UoCIo=beIIy8tr(YHd?kO zFE}4zSbD&q>5gWe8f}v<6xFc3Jsy)A1$I2r(WX6SU#XH++=w)`k62GzHo6#+KXT-V zbp?GRc4F zYk@5Z$0@i8@(_E5A{CE7(8(Ab4hEYYy80#^9TRCbtkG-f#8D?vx-n{vrbow>%Wd;+ z8<#2UI7f^yfr7OxRI0BwRzma#L(-wC(F~@emVrTdz=0FN3uRQnJuZ+ShctJn)R76D zE08_{A80kvp)=s2zglZFm(5^v@w?{J;lu4Q?xC3oFgxDOezx@hX32Xfkx7n3)^`b( zaQ9m$ta!&;u`IqA5k~rgd{hXw_fWI^(UGsfBQr|1hI=8Tl7lr#_!ibe{0UTu#Bg@t zE5t&BKI_g3l%+s=Vk{MGBe+t78G@ub@TSY7cr#RjH+7*xf57v;h&W9JgNlQ0-aDA_ zwou~L3S#%VMi(sP{SGLt2Okuh78@5%0O;U~o5i>0tgCf+yM?EgY-rMP2Fx9H_AguvcbmG=0=c;9q$&& z!mis8@YTZc@;{zT`e@s&U*f#2Fp$73Y6F#CrQ=c4JgMOD|vcNS$@ zd3+NSS(YBUSPsrOzYil1;{U;&%>=#NyY&E3585XGdv*%BUc1i!Gil`i8{Ypvcminq zf1t%~?f$`O6b}j4%}JG0kn?{zt0ZN3E^OOQ*AIr{FShX!Kxu+{t407cbZqUAL~?k| zqr8?sfuj=oNT5hX#2mXIw@}p#Ca+n9%%VTYn5&dR@Y?h~4#j3EVl;4?7+BT46gZVA zuyJ-px0MV-*xDtDpr*j4fSbctPkuZDlg~PjXThP8nFnNR`>dBQ;sbm;eT|hP_`^@wBb>mi7J5f;rPrybOKcH^n|P_4 zM`C8_Fh|wWz!Lvp>Bd`te_>Ks|s#RU5K{r?(keTy7z9u;E zIIbsx*l%Fp>*re8#b<*r{lR?UFYQ&)_#$`%YwPs;t*bJPBT?wpf?4caZRmN1NRV(1 z-CE;W^NN7iXwoo~6}7J}UbL+-o)i#L=;bRoP4aQ74fhI>sNR)UqoS^ry&7dPJszcU zs^_9KcFZTt)KI=W)b}srX$(iBw0UrWz6oVfFvjN*qb?Sx#D}d%fqtC8s?GvEU=|jx zoc%xg7wDCC3vMJvacOZtsA#D0Z81vEjFn%}lkR5;6z89N4Fe__N{$KMN zE5RgoY6tY?f86!&(*LuQL;e4Lu>ZdDKX|Om2mo_}Y|Ozg-2S1K3Ui@L0HE*SBA}-L z5Zx4!EeH~&Kfb3h5O|C#u^W*PWrH3rv)GU?;-+9~AMh+7m;hBpPAa>sdbP>c0mTCvKDIO@spIw6w z#Q(bXMukbN`71jU1AX{EJB_imRp!9mi%reOc*aXrs1LO{^hbizTqj|kn0x($@#%OKcxQtKo6sx*}MCPFyR|u<_y`GA@zAjnvHB#-3If)(TK6-H+iYdEhGJ28>Kn*k&IL-O5+!&45X z0$K{T9=s54VE8Hx-Wa9Mkc`WSH0M<|M2LgN=-?!M5WbKIB|IOOJph&841)7PK;nsB zq6ZoOYhYtU)RU=Mj|)l;md~V4L)x_Evx|?P;T0b9=<w-ce#Y`|q z2Q(9L!Xl9z2#cI}y6NF=3#{vjam*Zly#d5ZjOy=bqLA9xtG1Q2ZgR&q1QG_0cT&nt z^cFyPwFrW+77cU!QRvMSF9XpmpLa{1Ba3m2CbFPvXqxNznCQg92UJco?PdcY$+ z?06X=s1=^sY|(FoCp#nFZQPHu@t>AAcYRY+41zXBG)p55tCIi^L6C%on0OC~`UnGc z>0S&1?Wryx*7dFoarnsGy)cZ8Au@00=?hJKF}fm;aeeCci2EBbPx=h++K4!P5oge^mF3 z#VF%_Bu*$ckICnCr%}bZi?81nFD}f^UZ{K3>@iv>TL+9#eOkdKQ_s3;Xa>#4R-4Vb zKbuOGphF>I{uYx=ueJ)waxE2*Rth;G@KAIx9^=$K*a59N%)cgYLRWcPh9}D0DmiBig>&OfdW$TzN+Hg2l_!41O4Er+dhb%McX@mPr^LcS|%f4VIw&0ppZ80puDw zU#O;vIbU)al7V0h023X6!*Vaf)+b=;o+tNc8OqWI8W&@sNz&Yic87wGgCpPK{d^3A zYcAp4S!0|uY6t-}7I4SqM?&b+O$W4#*$mi$FoX%5zw)>-?FM$kAt8KYBV>4l@yOr= zNg|-l?FUv5NQUyk;(8jFcV~a>?yIWi%y;pIWwWx*tSFf;=ZuI;(&c%C~JuZ_zpj40b7UdBh>6g*DM z-z_2@<<`9k>*2fdRXFY5Jq~w|sVE3VM=t}zSX?@mUG$VbLUhZR3u9S&lUxY18Ieul zgn%^Rq)JG|V9(q5j}U(_KPJe5vm}XsY(fQ2E>|J15Nj(vW?RkD4A6)ll%DYz#B89u zURCIXKF|b@A(qjEfCdAB11?-y!i&SP%t&Ss3?A5zX}(Egu;LRWf$Q=_f7VxFMHOwq z43=e>VC{hWI@EoZ>v-x;D69z>h}!NXUOR=i3G1AdEBPdPP_t=__$Nr>(atU*$Bgu0 z7rAM3f>%Jk4)j)twK;DUYh~vaOk(-!z;lpyhF>s8+j&Vw%{m#fJ`c#m`p%mF`NK=}k0Na7=F`H$rL|gJI zL1;vfdg*h;@o5qxaF8V&*domXJV6Nf#zs{}l%@ipfr#SClQ-B{-zsxT03s|lbqM^( zF=&@miyQC>B5{#OKU_*dmJY~3w=BsUs#qgzv)EqjQrWAxzRhNpvfP%^L6n#Hm^>TI zxmwu}2WWLOP;IF9Qr0d@JuFDHXI({&(e(I@fARfgrC}~sIi5mb7 zm!&9)lw{_tyz+c6wpNV70lgjwsFP2#d|OvvL1O68|o z6GL3Hu`@|4Bme9hs3kOV5B~uK+X__@PsmIHBv+`vT$QFt^1G3%ZSkoXz_hyrrnV4% zUOaE`gjOn4&0~Dm%{3Hr+!2k)OdYe12jXC!8z0`V)n*aS3#6}Owdb|vW)U>qVqU?| zo2MoM=c>`LXT@_)PExT5B@i(eW+NI*28nnLUMJ*CGV1phM9acw>VPiYg*)(ES&kwn20^p=sMd znI1?rkkLst+puVe5rtpc_7^l4P$nwb!Lp>nOfc1kp6sg5#bk%dFJrwTVaSM%x@3bf4GU4ZTI8h?Yo4%=&7%n#<14KH@bO)?Kq$9W9>o zptdFY;fIkZKo}QKXJ?CMNMgv$E82`xJb1s>@C}1uU=aUrI}Up1u|I@jd;c%)25!Fp zhXlZxq5pTVCEQc1LC^m*dxvfilE@-@+p39VpU8E%LAU3#Gx|RgZY&`t*p4N25O%n( zc%UHm!|ZT7i(PEbo}AJnKv;vE@Y?{l2DB{)>bNx!4iUXI5P7(>TLXjeKhGES1v@+U z2?(w_nU`Q1Bzw;8X7^G!n(lHl%4)y{- zxdZ^9w(jR0Af23v5TH&Rz2;+(!fcaPJ1M88FM+5;qAjFc#-m9Roq=>3eOhIMC{ zX@W%4s3CnX1I~E##{0P%1JY!8A38a=d26-@?8nent_5QdAbd^TnR<;GL4I(6a4N8B z5MgP`TPLln44_1=UK%(w#E^I%9}t&;65$_>=^-rbg~#7$$J_I zF~lugdJSNW!Gq~d;K&4dr8o%ZkNJsOj$;ivg?U)z6v~w{$*5*su2qno-nTMo1jB~M zz0Jlras<;q%u$F522jGgYyiN1j`$V?=+=4GfniYXyF$y5P2sXd@T8nG3TQ9|B^X9Y zq-Wq?4gOlV%=sJh*#dcUCz;1B9#H<%JTH5EVfp0akIyY|(g_#ouB^l}H?AWb@M&wk zXeEl{;`%oUVUdX)-p=c1E}V^>q5EdmI@x68B9@97i;25Vq7m0IqJ1QClR3OKyc7fn zx6K-YIooONI0Q{16P%24j#tRRt5*`)mw%h$76$V@Q+XGN3o%;22R8 zMEdYrwTK1IfEfc|{;wjGRY_V*Q8@V3S;{p=Tr4P?CzlbgJL#PV8-{}kgRE$fEm(m} zBuvm}xVT6Te30ANaO_=TU{L+9;S~@`eh~U!CNnv?DgSRKhx(7h_@7-a>JGYf>VFHh z`bML?vf4zX%(#_Jr}Gp1?~L`DR<&HS7LaxV(O<{`0zt9_5n%ePP6H;vjFM`h3iZ-eljC_hrE#26Iu zo60s2x&WY^CU>FmYt?bm=Q%hz#24H-V*5yh*&M**f!7V=oJjl3LZxwhb3`}5W;;{zy zH1lkZZ;f%ab!M=1k+~K-tBIC9%w!~4vxCcY<`CD5vHlUt0xu~w*6 z%0PmUuw(#~^B*X0|^t^li#-vwdFnj^?UMFl%<<_YI8 zT(}ZDEU8Y;Lmg)cB15{SS*S0-e+zYi!EPkE63Pv1M2JhFJP`xVm&?^<{W>SB)y~Oc zq9f*TIY|z*pU`F@NH;^ekVM5DV+HZnZFm(xikRNO90O9J|3OFjfkTr4wI6`tfC3n+ z2K}<$U2n5PAawbadsE(84rDy_PPJAUIP3SxW?nu!oMzAb4T9%+3}p$VjB@ z8e@7@6(VL%T!F4BB{mUf1c3q1V4k7Dq<6D;yI^>^ikiFsh@o*qbz=;0Nen>woz6Rc+9I^+0-bHN{s zVCJk0A22s?S8H2mgcoFP7!)!phX3a^DJ4sMh=9Z^Tjpy>dP;f1{V*#}Usza{C>F66 z&893mg^xVzE3lPe_A3#n1P%7%FxEHN5{<$T%6x3ERd~x_hVlfqajOJpG=jQ()J4FC zs?vxi-Vy5pLxj6_4yqug@`7zZNA!*PByx=Mib*C6m0?RnDIlB{(-~!rc~F}m&(t*o znGTZR!7d#&l?~=Z+v2^U+$}=U1E5sRqHhR5nBc4NI<(jHWK0fd4BjfExd9F9B1s$| z@W(w5&G8h`#Ds$VJ*QDAQ|e+%!|4JHQ;oEa;+t>K?EMn$OuSI+Ix+y}7%dAH00&Tl zYnxboiS6dJ5%O!wl*yi8X2AFf*@JP5mcRf+mb}t3NJjC|GGMgnfwrrVqq13WR|w`R zQj%U7)tR(+v=|!qSH3Zzq6tMSH~2N`GstC3CbCTfLpYlMkQ$s!8g4@*aD$U#0xp|| z%g9Gn5rWs6U7ntZ7*_z=yNs%vpsp7p-EY$~iQgr51W(o7hcbr@Y{8GQpqxO;I0Dxo z(Ga3LJ0f$$k7C!VL(2bms_=3gx4#3ARv`xKd=*-tH}mcz^57{mxvkVJb3msw=05ZbLhg}fmHFM)>TUBsm_Fh{nK zOPgxanm89(F;gLG>ps%E2V*1WrE!|66OBbgf_>1;K)NbRkbH=Ax%HsSF03r;47!jU zbRg}(YY41}tP3SzwD6g*U>NnO^#`T;At==7iiiy5Vp4dJ;S52IVY7$lGjQwTxm3#| z^Wffy)FKN*XQjWXK-flQ>~W8Y|0RA#e8+gD;ZV^22(JdzQ0$}c{|%Q)j}#%rU-0jy z&uqmX9gNKX7p#+P)7b%y*d{tik2ZXSzo2CiqlN#sPU(;QT`xJb8+HzReN)3=K>k}0 z5ErCP(9#STZPshZlwckG`yw!C6B0-)7CSkCDRBQ<5I1P25K&QnQc_bet3s)K$HY;C zCFuh&jISE3kx3%{Pe~ut2Ncs7EVHNyE;e(W=^Jd?1jR^W&Wm3`F3cY)4t$7`U?-LL zEXaE(0SqYZEdrzl>xQD3ss$741wulN9yr!T_8XbRf=aA9YXKvCD`K_5PspGeJ7pTS zU%bQ(dJUHvkdZtyP9mnI85(Xv;WT-`|^VH7l2J1^4gI-# zTc;BGzvy;ndbdc5FN_uRWszNY3YqnWn*!BL`l>hQT{(GW>Cp?jPPzgRti)5k1z(d% zse*(w@L7r%$)s7CpNH8FcQRhT#5o3OCvXxB3WdShg69KYydKGYcvh=EX!U@NnOxBw zc%8NAFyM=!*w6+IKDD}iOYvjg}4F8*QM^Uz{%eQ)ZdpLq4*)Mz~ z?Dp~>He^tyYY)8auR?+NRJbslj2^>BW$Avd+! z#rjX@r!&L#--OBif3TzVPoa!Mb>}c}{kPNq^7&2qf7AKw)G+>Q=cwLs53bE1f0j4p zrl%&e1vfV{nah+i=_zN%^=7=IGuc9JYWiq?a=PRdjs}I97>03N&0BnY*w|@FJejwS zPUfelGl(m*9^$J_K_5gkYh3&|!gyl?ziy^b%$ANGEgp58VhP(!e#Xfb3MDU-%^xl0 zy{XAuDVufk1!LFm9IjdGRBYgvOqL3so130?bDmQGYTQX@GCx&xN=_*^S;$Xj)6=dq zGdaBnXcqQW)gFNV88x+Ix{7w-BJYgb}db3dPJ zvh#}1D_QWL<2)TF50dNLEPH}+<8zYRE=mn>z(0ayd!fVv-&_`~Ub z*gA{oClrp+Ykf$7=`-RA#}Es88q_YfswdVs?l!sfc{Q=dTb%|NMt?wqtNPX$G7st- z7`1^gsm_~QCIi_TPKFO#N0QfS&JBKeW?yxu{alAH)L7f||DIJpnN~mkPs7&htpAdk znoMVh`rmyZ3OM&>83Phrzo3ObWRm-U_-Dy0w&(wvTtDBN_qTuk7s!Wh{?F%fxuO5> zKG4WO>z|q_>;)9c{~hZ8a0yNh*MBcaAF%JB>z^9T%hvO){r3NCuKWByJDE%8hx>m| zZ0_&FJ1_9Q=Nn%9!UtaRrn~>+>gQke%Wr?rU&J0c{dd3o;eY(b|MFFtKl{Y!%l`61 z|K{mm`uAV?gCF_EpT6<+KYF8Pf8ATY>(_tvpDq5EH!ZyA%M0KCV(;Z&`3=rXzw(=& z{LXWqy78UAe)Bi$Zy))@J0HJ!*n42az4LSPGoSN*?PXv45C8UE`M-VNXFtC9_MfRv zAN{d+zpC}{C%$6j;s545`rY-XKlxjq`;El%U;oZuK9l{!e>MBRzr6m3r(V6k(;SYr z1qKG=|H;x8-3NL+l>Y;v=?l_m(DhFZcCOKL4!e>6*=fX!^&D-VA3o3HKPdm-7yA$C zmzZ|seD#0)=!I{8;e)Tf{Ou3ly!WAB`oH+h_4}I{HzyDR`w_JGQg)jZ`FZ=Y} z)u+}ffB2JM`**kgpV$7~|Ly!)-T(dn^A}(6d*@F5!9FkZaJb$W7>xh-b`G!$`49i^ zF#fCeitX>y!PcKOzjy!MU9CUzzYgR7c8~Kxdk(h#lV1S)KRsOkK^Jik?FL={)J%5D zEh2!WSjZ#H$4TeYh^;AB*ju^wa;+ndvV7Uv?4&V2Ao&Pi*e* z!#gi{X#6$juB2}sIux6D;S2V6@Zo6BdJGK4|K6Uq;$7_jL;vr6qMSk3KQ&nNXHTg6 z$^Xpsru^SH{vFo;?upEOehB;jyF>f`ew?A~^-+c6;kALEq&cE|daxW_UgTH@zrtrO=EdJe-ul?iS`S>sXfA9V6 z$+1jfvcEbz`M>S+Ooqel1_Rskzoz)v&A8D%{rEq-`Tl1t$Gq?;LCOfPIc5a+@eQ{GaX+4kMC#1v z=ZDRZA{?Vjvdy3;E}A253(43$+^S}hs-W>fO>j8wwir_eY?>A zav9`*-jmX~35qQqJkS0Az1Ba)XwNOswVTJgSpUg1;{S&0zZ-1ttk0I~pIW$Z?%d+i z@|n|%OXn^>va?Y;Tld@lxz=6rpVhPQMcxQspzpUv*^eF znRIvj7rejeq5i)w$nP`A>BUP6X!1ffTe@`S4E}lLp){2K$KH73{ii-qeeaLn{L{xi z|B)Yf^Iv~z@kNafpSW}H+uoUd$2WfK^anqd|L7~u{={e4arpO~#lY75zvr|cAx?Iq z|4k3|{}6u%`CkPC0d?z8?|*`%yc0IG|MrUc z{{078|J14e<80TWpZ=G|S%0_vmqjeWF#dBptnSZm?xdT}I7bWlQod9yWOMG+bkUsx z`(=+7-J|)Lv{yV@aP!{KOxz!QKiftbi2rFJ?BQ>B{6Eb9v0BD=ohGL<`9d+D9`gUdD1A;f9EksSm-a2h!*2M0dUBZmBShc< ze#jM_oR@RmOm=E!s^k<<2`87yxKr6lX9@wH`RSST(ezYyX2|~sxCmdsp$FpslY0Yj zH~gO+`hWL^^Z|Qw=g-YJxso?MIh8BsXNrYs9KKKIj=Iy+(^Dm+Q!LCJElmyie?Wvj z$C_=;|E&J7KcOJx{~hLkn8JQE^#9xkTI#d@sd~#_O*Cr>ssDoFb-Sqm)Mx#ta=A_S zKQj63(EoEEmj6(@{txn_ZC(CHBm0n+P3QojVju;?0(u_nRwG#(YUz+tv@ncRW(kVN zl&iO;S}5waqZMjW7rpXrFTopyEC4BW(WQ)-Q$#IzRF^<2fWpzFB3Teqh~p>}Mnz%7 zF@K!7<5)Sq9ki&`%VaQC&S7s1f^wG3P*+2VM=3NcMRgiZ6*aG=+z={Um@4eZM`vbc zab_%qhQ=7ez?*cv>c&C~i}Q#wLe7_(Uh4sJV*vXscgn9N#-vVw=rYZt4i=k1{0 zD~slw+CV9{l7qVHs8h(|+sKQ@siGSLD1DUJky>S17?zbi1H+7iG{9F z>!ytdDGcXS@sctMObWijI)&}IVu0vunMkkPJf>?=KmtI{T`G9?e!o^~!ys%Hl(E-A zB%vS5J0d((aV`~uq_Uf|u6pZ0HD}CPLgjjUDq1@;sbzfv;=$4(8K7VVA5qi0(Lf0! z8OdM~0~9>e<}9OZ-7!=wT9>+^0vLl!wS-c`L)l7%ureQEv~#;FhC#tOhb5#MP0m^h zfwL+k5*b!+d(|@fB6A_o%JM>3@qpA#jVMDWI=tdS>U{#KWg)kYYP1Gv1YU^=%fF(P zn{>uPukLJ2N1BUgq!v4>v7#oYR4Fzk<*T)7;tgH{h1{D?vn8~yWF*rR${Gt;%tg_7 zywR^o@w+tyK5B(MP6}lPv6ezztOwk^{-_GKm-Qz36)%adOs9$11dCfC6yQK7nP3-z>Ag1HG{){IekwhA*qSwji{jD(ha392e002}j_vdkbiphX~L zFD@$}S6XYX*YvH7+ooLT1xuL=@A*E680CEdG~Xc=O)&~P_!u0JV6OZYZ1xJ7{wA~& z7PtjfqZ}}cA)6IKq0=G?2M23xrorOYp$w5}x|EA7?1;L?UV|b^=S6@bG7H67*FCei z!AnuTi&eh@rUt?sa3qSB)fx%pE|p?nMea&Y)dz+tl|eO-G?hvqFBh=xE=i1xEw5Br z+!h4#3vhtatweKF_0g4w+(F(`NNFw!hhVeiW^LK^>Xq6CSfqs1r1Ewkf#q-^Pf$ap zz3nB7N?GSAx&_>NaZolUnbyCWiAX`u2XNC($y5KP7xOeT&ykY0DH+5Ov%C)k4kl1 zZ@cKC2(e9iYWaL1)qJtM2Eg=$Tq~ZY^yEo zRHkI-Q?7SARc%!&2562w$Dv}LT*#2` z@iF$s8&?yW#?eX`l`WC*n^-36csR~@52TqDNd!nMIzY4A6MJ#K5Wn=tVJ{4ZYsGrE zQ3-0tb^EiU<1R>1TS^e4)I|xG3{UAUXyu&lhYJ0ien&t1Pm#PEtFMigKK9>qW^?>E z>i-PwzcvOB?o-?TqnO&-04!jCzlBPeP8F;N^#$5HLc?@_f>z;CQ{x`60QCYtVk@vI zexm!HZFH>0N=r0x$vZ5y1pQSflc=FqwR`{ri_BcAp&Yf?R8qQ`8zjqBo9crQxKf=E z9B(U;qJ%2vuu!X($}9MQ!r%DA2ln)nQo=Pd0sO!kuNbFLt5rNIbRNtD=#z%9QIU9NV_>xGQhKlB$k2v8@w75m>acHqh7y5^2XxR4~37 z48BB$*bNY3(rokx!r|Lt_-#-L4neex~GLMX>|yLnPShOHp$I500X+*{Daw`BT6;3)9p`CD}-LnTJwIZPLs&I zP+mr>rn9pZPg}<^t$FLX&IccKGV>^;tFNI&mI;&+GYx|Y+e*}QUfR3;T?g4T&6W?X zZFZZtZhmR74kAgq1;?CfR_g!3f;Bhl-W+8}l({3~V>~7!D7!1c3^YWv!+5NlRQnCl z-M#|~@N&QY5tz~1^bV!PK)u7(iiMaB zB|%XQ+uP$YxlwG#BOPtpWA^4^w)PQJZMMv`(H{&+ zho(j|n2uTo2H^n*P6RKMQ3dz7K#m#$pcFrKWJ2c(q>sP{T1|B540wnd?v3WM8Eh_o z*L*sBxIM-_G!p@4$Gh3jwjP+XY~DkUOmZZ$zDuyCyWcut#XH`LW%0!gA*0J~u0pWA zhnnS&j(i0knNg}W+zTO<9IQ#gx3CuCPoP31hO+}-Ar>0+S$9^TECtdNW2s;p!IgT< z5G2)sH(egZo1qfCsS6eQ1D^Lq#A&bMi8E6-?;Xr|TPX2r1+jZwqYD=Deg~A+gAa;L zi;W8>0CaH0&Ei}0R%ne{+4%=En6>Uz(FIrn-baHjs56741CF~vkEoAIGd))9oH#eB zJPnA0$RNFuKvTDy#*Ok2l5DUsfllMfjUW{}-Yt@aUAH0Rg+R-2B5q<@zJn0$_R#mB zwC+hnHURypVaogZ+KXW;#Uk|4mqjap~nm_Povdy^B?Hycp95`6T)l9W!lB-Neu8aPljF8_MbEfsV9Y}P-Ky5sq1a)6 zqVv=EN+6WLZ*8XIF%nyt{lkcJQNh}xs%zsri!z-&zKMw}8wQ3RoNazTM(*eTLB5s* zJ=}Zo08tOxCjKKkmCJYC|H!6M0d#o(<312T+y4VCc5C+!PNR58xNc6WoPwPH%ULC< z1LmvM<@&*J{KYmt0w_&TZ`BBZx`nMBl1L7(_2hB_Mt@C&m94eW4K(@Bedif%bTkt5w-5Lz}!&W@Yu2(KSPUUd>wrGucauoI_SVXv< zr8pKH0&ajm{DeKi3A}2dcf@Q!N zJNKQvkxkfa5(va>ARB{Zlg;eRzsZo@na#{@k~IVi*cJ=oCDy_NwiZRS*0vfEr3C?> zM^Vw@1+CgrZw0*Zc(1iS+V}mw^Z#?d?Cxv?oPCt+{{K1WKj%B&_nq&4Sk#FwF|&18 zqe|(tc+o>mm%YwbfyCL1IuCOOeT_6m(7%kC5kT0rR3et48EhIeGyXn)#&LM8QI8t#16YofD3VTf9(g7~UZGkguAK{am52X(M6h<&o2M}6>aAIrm zfY39`4Vasq-(NU7bR&ENmRwP>saj$G%n*2?J8yJmY7&!>{w-h1B4uJ@r>(X<=s@#< zOrF}m3l%iWxs{4k49Go}cT;*6rnlS&i7;k**u+jz8Vy|{_yM5E7hExOq4lSaY*f$r zAEQwZj@$SGrGOgef3Q1ReE(ZG6v6#p&HT@$7XWhSe|7&)6#+o$5iTLHTZRC**rMJc zI96;QGKeM}=B#JZ2spN!zTomS9Sa=^w=O=!)ya^|W@pJ=ViX4O4cJx?afb*BhvkEH z(%%5vi-bYJ$v!Q|f=@=*FEM$ndIq*jDwC8LZ z;6iB0NC&kD2RN8E?tm|w{M~m1Y{(tLOS=eTJ_;YU9u2Mbww~`j=ILKDM|M5*KV@h3 zISW{b9#E71QR}NP|2rJQ1>jBkUwZP-i~n6b{e#AelmHMXP{%laVfJ^WRHzF@3;=!W zX8~myfXJqZY(bDH?eS%qfy~E<5({Ao`J_;WDdcF|aa@SMH;)03)ID%K%a0-XifxyW z?j6*sN&mR4+o#r}g^<2{rWXB&dy4OW4hOrN^zUrZlKxxo`H!;-nlQ8jMDWE?D;7G8 zDxc@XSJ&N5Q}$(zGCQkc*PeJ+ZGIDIpaMG!AIkzTSx zaY-Tm)Ec}d{p-FP@x?cl-8X+WP=o%%J;?PbI{(9=a5Mk=WMImC>7Rxc6=n17EoDxA zZDfvq_L8_QIvnu3MsCrVNDCjHfJ2y3jX2+FOmN^d&?(p_-(7Ep(Jts~I>FJNMIK1p z&J!>2Y6y=7MD3Bwy-i*mx4;!4&FmkFfoc<>5#M{K)oxiZHQ~b!X-d2f35~z zh*9t&FnMy?0ic;V2F*gdfgL27`!M+X>ECQKR7=Z2_-xq$sU+h zcE$3TD&U%DG4+2f`vdubi=AfrWP6*@2bgr~Cx56K(n)O?0N-(UjNn zTYGt88w7C!#|trKFM766P{(WoRDfFrkyo#_25{*>xyf7bL}E6pI`kJO8fW|PdSeVk z5}w4K>C#3vUOKjU!^W*6h|-X#qLkH^4eS|h_pe$z+TmZ-8Yn=3erq%om_|s=N(oVr z{1iw%Gl)oMVnHSgTu-Ka088A)ujVOWjPHBPfm))1R3l&%UIDyGCO|aeRW;z|!4!Zx zMKhBH)%-RE^B~391+8EX>jWn!I0z3pj5*lLS zJtXSG4b-K1(FwGtxPVyKqcX(GRRhP|peTmOAli)3r9&j=xDY-H4+)rPd(v8Mg*xzF z+!*uZB_{)4o8M?G1ZwgBiR@H5#l!~RVLE^i5I+B7pgR8#g+jRNxmf=Tqb5Yt|I^sL zE;kzVzkJ!#;~|gFqHscd<~;fRa4eI8-zBSmZQeH2-#QB*>KU3DhU^q9d|$^ zG7XlX{sHBbH31|IogY+H#rT&J49P$+1}2j~0EgvShNDlWrDvYx(K3`JH)$lsLXxDq zk>4ExJ{(6`i~0E&2H7@l?yONxGHHZ>8V$H0`H?fs(o6@ii`tC217HXf*t%njQSItB z!yTNpGjlG{BaBA|=aeJ@+GIcQ_AtN8BP{M`fY8qd3j5=9_e{-M?X6}f5RXmBkLk*h zJ`Kos;3}~52QR(g)q+|(Qbr9F^J2mtpWU}lcVC6#?IRmDUV14G-JssFP@%VjPTU_LivlH#D z=?>Y279~5x%VbeE1&@7ekH!&?a^P5}b^cNLQz-4&F%EZ(r6@Ryf?j40?XA)=nLtnJ zBS<%fwJ?^YJj%IqHY2hrj1b@^T+{?gF?iQ{@gG5cXMHS?17nFF|9CkoP;xQ_euY?D z>CrzsGtmn$;)UWf9s`-p>_$^MJHZb$!DFyxG~q;@fj|MnJ4WzfcPulK83cnn`eTq^ z(ip7PPNKl5Jki(sDeS0%%~`>i4CCw_U_T9UpV##rJmNBIP7FxxEks^#0AG_F{Y{dR zPoxJno5qNL0w-=O?BY^POAT_7Oq+E~0SOWoAcolt0(2bo_N@gw_N^sweQOi2VI}N# zP>jwU`%t__!9r(^)0oTQ-adfX(llw{}`>4sb2aW?dV@i3gL|ZbIz*QoMy|lT)ed@;u9ApFzTa}C zfrwe<=}?FY}7@8Zy1xAn9X2XQql*9 zddvhZDqDg0AWSO%SQ3*UsD;!+E(NZFe(j~;?6i>Rg=$vzL6pAmD$^=$C4`4ay5fUx zR6fvB@WOoh;K5x$9)&S5sibS?ZcGiyQshL6G6U9{HQttuTQ_jhExWw(lzA8=qq<6ztbL>+gfMpjkOl=|bym_mE6Iv-#HIMPP9^ZvOP3}LXQz>{`^9ojN>a86ArLVa<{+w%2MK$1 zJ}2l*G^+I#)Od?clCR5|2RwB~sPm(0-p+Ni2Cjltc=RaFkp;8(auS7M^pGHAqR!bb zJCCRlFs}hfDe?{?hCIP;3+WrNYpoR&R@JJ_32XB!raTou*6%nN6c`mCke)=n8xhw1 zt8g-a6S6*l1@kis*lG5m5P+S4&T|xGqu=oP@X2Ci%s+FbQwcru%q>22#c1aenf9xA z@YW%#7975quY!s|tM~jxrz(IhMl@~f2-9_m1~NL)rV5F=5YhQd+uDpr8@mEsF_XdH zI}IH|frauHn>-wwV^u$qP;%-Z%Tj^&sn0H;|RZC z%rl2a-$^D&K>fB+{fCEm@=!8?yaG9habgOd7RdD?5sF-2ZL86n_(?^9kJ@2$y6lV-+zHU zGjup%Hmjh;RS4h-jE)9I1LP~mG!OBpKtOd%^uvpm{0U)PJnip~n;{7yv#$JRTw*y( zRP)*I0|K?|zquIbg~k4GP1Wtc$PFyb{|iNMCsfn^yYv`QZ~Tu|@6ZE+QSbTDKPZO) z^Bv-JX!d++M*ru98%u}@j$;J?EeXN^)&DQcHp;^|JF?i8Ay%3o(qz&o-^+kAy55+dt1%!m4DUmy*db=kmYMr`Xe(*K z7z6;HQgf!JF(b%N89WcIT(r91+Q|S))au1r_zJ#A{(`s^{DpDES|n<`WRGgJ3JjFm zI*agkC`tR_WQ)VHC>j`MX<|eXBA5W!AeZlgf-WF;08&sr#X=GY6uzwm6@X224bo>3 zV9y8`l#|8!QMe|95JP0)(rN&03>Hkg6D||fmEs^=Kh`H=IoujF3e#B?8&6IqiAHH_ zdwLSp>9bZSh+tUP-8+-%K#gG9hdBx%!2n8Fm(2ulo+G{m0lL%lbzqnY>afOU3X?5@ zC&kYwQ-dlf!7z#l$gj9i-ih7t{!Wo0)sUnz|-upesA<830 zak*-RO9$VCf;|L{6Y0aWYGDgp0V@VT{ojvJRz+!1MPcAqV=2)Aak0Q`dM@*259xaW3UC)*O3*5U5A~%h=B0_L^}>`;QII68b{xhk%}ETaJm$>V!MQj^JE9he0u=zPR{jK8?a8?L7^Wn03pu$d zg@-9gimHfBZ7KW6Oq(+^@$|y1D4;VFMAaRWVIi^#rzW@W9F~-P_%IM}!6VWlaxL7c+9f_e$~U>)|Eli4SC)8T5s!079$Cp6I_-vfVf^1kJQR~EN!dh zMfBZX9x>Uusqyq=G7emukmBxpVd*pnThKZWWnOr40uwk}NEVq{$N=u75jv)|>!n0K zk3kb>D_@W}+q?;^TNhxcEy88_kK`m*{DcSt^c?5aNZZOIU5{5}f=J|#1hhep%iFs` zM8USvEr5yz@Z?Xy_75jK@?jv5%vi%0Qxwq+!{6(8h;LwUHIYb%1fud%56m@-_X03f z%@JVXMg?!HStpFcFyTtRP2jqLxjf|3S+4T{ z=3~j!nEp8+yOpcSVxl7kkennA?LFGeozh+xFQia$!B|ebb#A^oL9&?glQ|DSh5QF; z8AW~IdrQvv-TP?_l4zM?L3X^>qEmn)2neIXSt{55_@(f0-rDJh$y+)Re4XTp!HGgq z&6Kbp`boSVkY7p3@1fK?#tvhlBH*~N*yHR^>rq2O$C$AKd2(-F_i%;obld4qneLEh zO^kZpJhT{4?g0o6Ab_%Jz%Tn<^fj+LN!`J{;(U;?^LI`W%`H<@ZtENl0-qAlS|UJ% zJak+J!3)WS=C@%^MIt3v56i166ESPz4iqiPSAv{*0CZ>u>vR<+&CO!Ule)c>F5vuR z%;K&fBQeAs27?_q94ZeRe3iOqQYr*$fvH%UW74wa4oB~Y!`uwhes12>>8YNIO^cVf zC9tR6DFVE@!xF!5OFPPv4Xi~73VR`Ys9Sa5mR&k>YgpD-cI@zO6Q?H*sdI!4Wl>X7IRim|R6gii3n(2^hg6F?uV~!V zRkfI4H51p_iZ@&fexL-iW~KXpwSkLTdv!(lKxnrCA){jWf2K((TH=?yDKTZs{OO{e zVxCYx)XELRLt_%fBHE%kltre+2keMqqGxK91KEtYuI` z>4D80n1C@FL0wtgi+}@Fxe-;oRn{;=go{oNiXi6Z1+SSpqHoM^!pF$3SY*Or8IHs> zPZCkIt#%t?6Zn~$X28>d5_Ig+P%}BlnrIci>+;<=1U*cYqFMeIoGEnj)A$_PYk4vj z2Q&(AiqYJd4ev#gI6%OUXC9j4DWZuv3--5TnaL!%E{-&K7oeDGq;)>L`30@sFVW6~ z3)yZ=2EZEguLW}^hp9Meo7jDc?dGx(@*8LAI9#g4&^WUK#!tu@j4WDC22QZ}y>CYwxxk&h}OIG;7AJS`C(42gqEN7X}6_Y0BkhiI9^dx;&v zQ#JR^X2h0(mmaq!yD8G5*G5XXD1`~87SzYwcZY(@}zSO6|EjXZ(je17-$+@ ziZGlUQ^2SJ^Q<8wSfdlh7QO}qw2vTD;JJ{K`|*VQvZ&Dno1ud~a!ebk7UA_u9tcR~ zwI4W%OlqyDTy*|6*pr(_8-0H@*rG-JclkIds4Y(YCB*DFI+09q_P zxwIhzk1(Q=-=aFE1k8~w>s2hxi97FT=t_ZRbVfPZM|zhbY}C9oMl&^{@e&c|95gGC zu1XR_9|B#n9(3DwHY_VDRU*c!Pc8pIN3I*=B zFf|~Jdjx9!-%zRa2ymwOasDm&ovrvXhLQRIachum3Ok??+vE>YrVStA$7vbIXzu^5 z_4<P%nf z(B@POH|DzdC(woUL&bm(R^ptb@|_|14mp6CO74gNszJLUE2e0{!g>yqP^AZkb>aPn zC-Fih-a2amBK#_1wLwp)pz2v~8s7TpG?%L{3jPgm(&7+p-C*6rWz3W3}cL(GBtV8I~A*iVhB@&rnp{c*DH9rr~qV)|qteUo^Y3yjxzGFO22b&w0r%EQPFk!$W~$CT-QFbyo&=j9fUp z;G!#-g5^7ywP0&9B~?(61~yBvBAGHPYu7?;hdCLa7d6f)I{{BHFcb>sEG!@R!!A?Y zhX=b_!_)%SZfZprz_s6^!GIrzU_%=mOmXmi;9lCeQ$_|BpunD8R?F^HA^tazXv4K- zMYh!q)#u^3WPksWu$B2g^hf84u0?Vp#(#CQp0oZG{~JUFfFl1NZhr_jG@}g>*-u=43{gi7|nf`H|Lbfr}CD~WG|AnuM>A$O| ztNWB_V?JMf6qH#&)iA4iqdMMl75H(PTOD1V3Qel-e}I0`MNgA(ndkmTdxFvC{x2Hx zwFjtU|J5;Hd#H*n7O?-_Jw2i3{#S%(<6hLg|AF-j{wVjqn*7&=T%W@FPobVrw0Zx> zf&o5{j@+Aiek?m0kA>sGo_KVkD;SB|-SNMedf-{@(6JXve?b&VtwP?6WtT> z@o*#&?TII%-NA6UFP`Y@?hV@UzVSr2Q}K5`Q_~gnTkD^x@@@U}KQs@lDs0M4u1^2O z^}j*?-A(xx8zk)YjccLAZs#AWXrFDClKz##kkO=haUD2r z2e)s0Mv~jS_U2?XuAlw~7d#!}Pc{1Q?kTSS%JaV||1TKe^XSOEspn@Y>A&px&*R2Z z_xWG9uAsbTKGKPE{tx;>^{XV5L?XSh$b{X~6^+EZd*kChkx*Apq%YCa(-WQO?v0K2 z_Dw_=A&(6D!VO@NHRzvG*gUYQd{g3A75WdC-v0&dKiaJSTppT@eOeFgv&~Y{e}(hE zZsM=I;R1-CR}dp+vma%f4>8VGY!l>7bc7An&!FNbLqVzgBPShV5_UFNKe+xt`FY*P z#!|wkEXofO-`CfF05)xMjpfrzHCpBKpEoQv=BqqE=IQ@~q}25PH3s_CN2$L50sDwt zJXd{;T5q}kRph^51h(EH|4(q*>t$z?S(ao${2b!x5qT(gotdroQQ{ep z+Vj-x@cj>V-QJPhq%w(AT0o^T?wc2s>fPR5l8%UEXtlVNMkWV2pe5JL@ zzbhTv&0nr+fX37as6Tbas7(J2QT=40wdfy?x?=k8>gs9I|H;4-1o-qb2gE7A@QihI zo(y0<4Zl&D{?k%=Zt>Aywd=ovKNQn{6rQ_g{Le|ir<(g8Kq00@{*eFAQ~r;3!~WNd z|6C;C>*!w3{s-!_Wmo)Kt@FRs{|l3FPxJoQiU4ikizCbI8}Ii#5L~`t;PKU&GwkE3 z?v@o_82;MiUG10cTK`Au{_C1cPyhUXpZ2WNH$9fTJNko{|M2o1cV70^)306r+KzXh z8(F^pzVLgl{?Kr;^Y3q3_PGakyyA0LEdS-HmppLY!_PkV;JMpl)1g0p@MSN1%b}Z3 zTlbCGfBfb3zx~9CdyicHak>6ha@kiG6Y%5oALj)?PK)=J7Kc_ zvH{Wdjryr;P=}6zgYhZBW$b5{|kV)M1PR~U+g(Gw0y(v$5(vqd0+g} z_Y(cV>%O`9r{8<$-doSv@Z(=SXT|QzZrQnF&3ixmmDm30tg8>-^M?I@c|&*G1vlMx z{E27vF2DGL(}&K!`QjDloc`{QJpA29@7#RA(5zOTK^T_|WA)xa4T&#y2ec`PrMkeCDU_^1teLU;pU`Uiajh z;a|-@@|t^2nR)a2mL*znbJzk1)aL(*u7VB=_@l}H7XY-A)TN&N57ZZ4Uvb8@`G0Z# zM>x_IY}Ws+2+#(;D3bpZ$6t9%=ZY8nE^#7tN$?}Df9_THKKu5wZ~g2`pL6yN58e6b z-M@JA;d|cq^=%(|(;4@F^Y(YXc>Mf_&-~2W0vE1*{C)3z&NX{qoci?>uYT7*pK;*} z?>J>>WW~des{wJR}e(y*9hX)=Uebd`d9e@5y?t3!! z#@Br67rVYX@w%!1`2OGC^46c-a^Ulioi=r6ll?bvkE;z;oBkK?2VMaGhySmc|66U} z8uhOB{^P=n1B?6Gg7zOdfld8yad5AvckTV}8n0)ZYHb#<|IwbVU~~Vg0kuKz>e>H5 zZ#bHWcXx&3aKL4o?fBE>&pLp3h z%N`uK;kwX`>37`nn>**;|GFo;I_~+{w=>(mecI37y?#&Q7TFxBA_QvFe*{KrQ4|4y8d zIO~otwXC@0{udp&ru7}S?Y*$&1#9kIcDw&KxBSol{`gm+J7)gr;h$W!{rXkgmVaaT zEua46eP`Wz!KNQ|JQV)m32%1Wxqp%P&51`dR|h_M`kQ|J)8yO}{>Nrcx!|Go*F1OE zWN2z~Y{&C{^YxE@W!>R3`~T(c$F974&qFW0btc+~_`=3P;}377%Ji?-do%(fHR->! z{wvDLH2wdLfPJkocHzz)d$*@0SIQN!AgIKT8-%h++(|cJ1$mnmu8YEjGWd)4LVH@q zdF9{$Zv@WbFH{OeeQaJNFwOhCvTie65scC0vOT9K3QZ$#V>+F+QB|3j)Hsc6O|TuZ_{{0wpe5F%f5>`; z`X4Ig^fc{%4FZ4FVR(g;BkP6zkv@OaY9E}Qp0uqCfCs3<)6qA%O(Vx5NJZ+ IX$pb=2j>XW*Z=?k literal 0 HcmV?d00001 diff --git a/gix-revision/tests/fixtures/merge_base_octopus_repos.sh b/gix-revision/tests/fixtures/merge_base_octopus_repos.sh new file mode 100644 index 00000000000..94b6b73435b --- /dev/null +++ b/gix-revision/tests/fixtures/merge_base_octopus_repos.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +git init three-sequential-commits +(cd three-sequential-commits + git commit -m "A" --allow-empty + git commit -m "B" --allow-empty + git commit -m "C" --allow-empty +) + +git init three-parallel-commits +(cd three-parallel-commits + git commit -m "BASE" --allow-empty + git branch A + git branch B + git branch C + + git checkout A + git commit -m "A" --allow-empty + + git checkout B + git commit -m "B" --allow-empty + + git checkout C + git commit -m "C" --allow-empty +) + +git init three-forked-commits +(cd three-forked-commits + git commit -m "BASE" --allow-empty + git branch A + + git checkout -b C + git commit -m "C" --allow-empty + + git checkout A + git commit -m "A-1" --allow-empty + git branch B + git commit -m "A-2" --allow-empty + + git checkout B + git commit -m "B" --allow-empty +) diff --git a/gix-revision/tests/merge_base/mod.rs b/gix-revision/tests/merge_base/mod.rs deleted file mode 100644 index a4bb86224d8..00000000000 --- a/gix-revision/tests/merge_base/mod.rs +++ /dev/null @@ -1,90 +0,0 @@ -mod baseline { - use bstr::ByteSlice; - use gix_hash::ObjectId; - use gix_revision::merge_base; - use std::ffi::OsStr; - use std::path::{Path, PathBuf}; - - #[test] - fn validate() -> crate::Result { - let root = gix_testtools::scripted_fixture_read_only("make_merge_base_repos.sh")?; - let mut count = 0; - let odb = gix_odb::at(root.join(".git/objects"))?; - for baseline_path in expectation_paths(&root)? { - count += 1; - for use_commitgraph in [false, true] { - let cache = use_commitgraph - .then(|| gix_commitgraph::Graph::from_info_dir(&odb.store_ref().path().join("info")).unwrap()); - for expected in parse_expectations(&baseline_path)? { - let mut graph = gix_revision::Graph::new(&odb, cache.as_ref()); - let actual = merge_base(expected.first, &expected.others, &mut graph)?; - assert_eq!( - actual, - expected.bases, - "sample {file:?}:{input}", - file = baseline_path.with_extension("").file_name(), - input = expected.plain_input - ); - } - let mut graph = gix_revision::Graph::new(&odb, cache.as_ref()); - for expected in parse_expectations(&baseline_path)? { - let actual = merge_base(expected.first, &expected.others, &mut graph)?; - assert_eq!( - actual, - expected.bases, - "sample (reused graph) {file:?}:{input}", - file = baseline_path.with_extension("").file_name(), - input = expected.plain_input - ); - } - } - } - assert_ne!(count, 0, "there must be at least one baseline"); - Ok(()) - } - - /// The expectation as produced by Git itself - #[derive(Debug)] - struct Expectation { - plain_input: String, - first: ObjectId, - others: Vec, - bases: Option>, - } - - fn parse_expectations(baseline: &Path) -> std::io::Result> { - let lines = std::fs::read(baseline)?; - let mut lines = lines.lines(); - let mut out = Vec::new(); - while let Some(plain_input) = lines.next() { - let plain_input = plain_input.to_str_lossy().into_owned(); - let mut input = lines - .next() - .expect("second line is resolved input objects") - .split(|b| *b == b' '); - let first = ObjectId::from_hex(input.next().expect("at least one object")).unwrap(); - let others = input.map(|hex_id| ObjectId::from_hex(hex_id).unwrap()).collect(); - let bases: Vec<_> = lines - .by_ref() - .take_while(|l| !l.is_empty()) - .map(|hex_id| ObjectId::from_hex(hex_id).unwrap()) - .collect(); - out.push(Expectation { - plain_input, - first, - others, - bases: if bases.is_empty() { None } else { Some(bases) }, - }); - } - Ok(out) - } - - fn expectation_paths(root: &Path) -> std::io::Result> { - let mut out: Vec<_> = std::fs::read_dir(root)? - .map(Result::unwrap) - .filter_map(|e| (e.path().extension() == Some(OsStr::new("baseline"))).then(|| e.path())) - .collect(); - out.sort(); - Ok(out) - } -} diff --git a/gix-revision/tests/describe/format.rs b/gix-revision/tests/revision/describe/format.rs similarity index 100% rename from gix-revision/tests/describe/format.rs rename to gix-revision/tests/revision/describe/format.rs diff --git a/gix-revision/tests/describe/mod.rs b/gix-revision/tests/revision/describe/mod.rs similarity index 100% rename from gix-revision/tests/describe/mod.rs rename to gix-revision/tests/revision/describe/mod.rs diff --git a/gix-revision/tests/revision.rs b/gix-revision/tests/revision/main.rs similarity index 100% rename from gix-revision/tests/revision.rs rename to gix-revision/tests/revision/main.rs diff --git a/gix-revision/tests/revision/merge_base/mod.rs b/gix-revision/tests/revision/merge_base/mod.rs new file mode 100644 index 00000000000..565c6298447 --- /dev/null +++ b/gix-revision/tests/revision/merge_base/mod.rs @@ -0,0 +1,157 @@ +use gix_revision::merge_base; + +#[test] +fn validate() -> crate::Result { + let root = gix_testtools::scripted_fixture_read_only("make_merge_base_repos.sh")?; + let mut count = 0; + let odb = gix_odb::at(root.join(".git/objects"))?; + for baseline_path in baseline::expectation_paths(&root)? { + count += 1; + for use_commitgraph in [false, true] { + let cache = use_commitgraph + .then(|| gix_commitgraph::Graph::from_info_dir(&odb.store_ref().path().join("info")).unwrap()); + for expected in baseline::parse_expectations(&baseline_path)? { + let mut graph = gix_revision::Graph::new(&odb, cache.as_ref()); + let actual = merge_base(expected.first, &expected.others, &mut graph)?; + assert_eq!( + actual, + expected.bases, + "sample {file:?}:{input}", + file = baseline_path.with_extension("").file_name(), + input = expected.plain_input + ); + } + let mut graph = gix_revision::Graph::new(&odb, cache.as_ref()); + for expected in baseline::parse_expectations(&baseline_path)? { + let actual = merge_base(expected.first, &expected.others, &mut graph)?; + assert_eq!( + actual, + expected.bases, + "sample (reused graph) {file:?}:{input}", + file = baseline_path.with_extension("").file_name(), + input = expected.plain_input + ); + } + } + } + assert_ne!(count, 0, "there must be at least one baseline"); + Ok(()) +} + +mod octopus { + use crate::hex_to_id; + + #[test] + fn three_sequential_commits() -> crate::Result { + let odb = odb_at("three-sequential-commits")?; + let mut graph = gix_revision::Graph::new(&odb, None); + let first_commit = hex_to_id("e5d0542bd38431f105a8de8e982b3579647feb9f"); + let mut heads = vec![ + hex_to_id("4fbed377d3eab982d4a465cafaf34b64207da847"), + hex_to_id("8bc2f99c9aacf07568a2bbfe1269f6e543f22d6b"), + first_commit, + ]; + let mut heap = permutohedron::Heap::new(&mut heads); + while let Some(heads) = heap.next_permutation() { + let actual = gix_revision::merge_base::octopus(*heads.first().unwrap(), &heads[1..], &mut graph)? + .expect("a merge base"); + assert_eq!(actual, first_commit); + } + Ok(()) + } + + #[test] + fn three_parallel_commits() -> crate::Result { + let odb = odb_at("three-parallel-commits")?; + let mut graph = gix_revision::Graph::new(&odb, None); + let base = hex_to_id("3ca3e3dd12585fabbef311d524a5e54678090528"); + let mut heads = vec![ + hex_to_id("4ce66b336dff547fdeb6cd86e04c617c8d998ff5"), + hex_to_id("6291f6d7da04208dc4ccbbdf9fda98ac9ae67bc0"), + hex_to_id("c507d5413da00c32e5de1ea433030e8e4716bc60"), + ]; + let mut heap = permutohedron::Heap::new(&mut heads); + while let Some(heads) = heap.next_permutation() { + let actual = gix_revision::merge_base::octopus(*heads.first().unwrap(), &heads[1..], &mut graph)? + .expect("a merge base"); + assert_eq!(actual, base); + } + Ok(()) + } + + #[test] + fn three_forked_commits() -> crate::Result { + let odb = odb_at("three-forked-commits")?; + let mut graph = gix_revision::Graph::new(&odb, None); + let base = hex_to_id("3ca3e3dd12585fabbef311d524a5e54678090528"); + let mut heads = vec![ + hex_to_id("413d38a3fe7453c68cb7314739d7775f68ab89f5"), + hex_to_id("d4d01a9b6f6fcb23d57cd560229cd9680ec9bd6e"), + hex_to_id("c507d5413da00c32e5de1ea433030e8e4716bc60"), + ]; + let mut heap = permutohedron::Heap::new(&mut heads); + while let Some(heads) = heap.next_permutation() { + let actual = gix_revision::merge_base::octopus(*heads.first().unwrap(), &heads[1..], &mut graph)? + .expect("a merge base"); + assert_eq!(actual, base); + } + Ok(()) + } + + fn odb_at(name: &str) -> crate::Result { + let root = gix_testtools::scripted_fixture_read_only("merge_base_octopus_repos.sh")?; + Ok(gix_odb::at(root.join(name).join(".git/objects"))?) + } +} + +mod baseline { + use bstr::ByteSlice; + use gix_hash::ObjectId; + use std::ffi::OsStr; + use std::path::{Path, PathBuf}; + + /// The expectation as produced by Git itself + #[derive(Debug)] + pub struct Expectation { + pub plain_input: String, + pub first: ObjectId, + pub others: Vec, + pub bases: Option>, + } + + pub fn parse_expectations(baseline: &Path) -> std::io::Result> { + let lines = std::fs::read(baseline)?; + let mut lines = lines.lines(); + let mut out = Vec::new(); + while let Some(plain_input) = lines.next() { + let plain_input = plain_input.to_str_lossy().into_owned(); + let mut input = lines + .next() + .expect("second line is resolved input objects") + .split(|b| *b == b' '); + let first = ObjectId::from_hex(input.next().expect("at least one object")).unwrap(); + let others = input.map(|hex_id| ObjectId::from_hex(hex_id).unwrap()).collect(); + let bases: Vec<_> = lines + .by_ref() + .take_while(|l| !l.is_empty()) + .map(|hex_id| ObjectId::from_hex(hex_id).unwrap()) + .collect(); + out.push(Expectation { + plain_input, + first, + others, + bases: if bases.is_empty() { None } else { Some(bases) }, + }); + } + Ok(out) + } + + pub fn expectation_paths(root: &Path) -> std::io::Result> { + let mut out: Vec<_> = std::fs::read_dir(root)? + .map(Result::unwrap) + .filter_map(|e| (e.path().extension() == Some(OsStr::new("baseline"))).then(|| e.path())) + .collect(); + out.sort(); + Ok(out) + } +} diff --git a/gix-revision/tests/spec/display.rs b/gix-revision/tests/revision/spec/display.rs similarity index 100% rename from gix-revision/tests/spec/display.rs rename to gix-revision/tests/revision/spec/display.rs diff --git a/gix-revision/tests/spec/mod.rs b/gix-revision/tests/revision/spec/mod.rs similarity index 100% rename from gix-revision/tests/spec/mod.rs rename to gix-revision/tests/revision/spec/mod.rs diff --git a/gix-revision/tests/spec/parse/anchor/at_symbol.rs b/gix-revision/tests/revision/spec/parse/anchor/at_symbol.rs similarity index 100% rename from gix-revision/tests/spec/parse/anchor/at_symbol.rs rename to gix-revision/tests/revision/spec/parse/anchor/at_symbol.rs diff --git a/gix-revision/tests/spec/parse/anchor/colon_symbol.rs b/gix-revision/tests/revision/spec/parse/anchor/colon_symbol.rs similarity index 100% rename from gix-revision/tests/spec/parse/anchor/colon_symbol.rs rename to gix-revision/tests/revision/spec/parse/anchor/colon_symbol.rs diff --git a/gix-revision/tests/spec/parse/anchor/describe.rs b/gix-revision/tests/revision/spec/parse/anchor/describe.rs similarity index 100% rename from gix-revision/tests/spec/parse/anchor/describe.rs rename to gix-revision/tests/revision/spec/parse/anchor/describe.rs diff --git a/gix-revision/tests/spec/parse/anchor/hash.rs b/gix-revision/tests/revision/spec/parse/anchor/hash.rs similarity index 100% rename from gix-revision/tests/spec/parse/anchor/hash.rs rename to gix-revision/tests/revision/spec/parse/anchor/hash.rs diff --git a/gix-revision/tests/spec/parse/anchor/mod.rs b/gix-revision/tests/revision/spec/parse/anchor/mod.rs similarity index 100% rename from gix-revision/tests/spec/parse/anchor/mod.rs rename to gix-revision/tests/revision/spec/parse/anchor/mod.rs diff --git a/gix-revision/tests/spec/parse/anchor/refnames.rs b/gix-revision/tests/revision/spec/parse/anchor/refnames.rs similarity index 100% rename from gix-revision/tests/spec/parse/anchor/refnames.rs rename to gix-revision/tests/revision/spec/parse/anchor/refnames.rs diff --git a/gix-revision/tests/spec/parse/kind.rs b/gix-revision/tests/revision/spec/parse/kind.rs similarity index 100% rename from gix-revision/tests/spec/parse/kind.rs rename to gix-revision/tests/revision/spec/parse/kind.rs diff --git a/gix-revision/tests/spec/parse/mod.rs b/gix-revision/tests/revision/spec/parse/mod.rs similarity index 100% rename from gix-revision/tests/spec/parse/mod.rs rename to gix-revision/tests/revision/spec/parse/mod.rs diff --git a/gix-revision/tests/spec/parse/navigate/caret_symbol.rs b/gix-revision/tests/revision/spec/parse/navigate/caret_symbol.rs similarity index 100% rename from gix-revision/tests/spec/parse/navigate/caret_symbol.rs rename to gix-revision/tests/revision/spec/parse/navigate/caret_symbol.rs diff --git a/gix-revision/tests/spec/parse/navigate/colon_symbol.rs b/gix-revision/tests/revision/spec/parse/navigate/colon_symbol.rs similarity index 100% rename from gix-revision/tests/spec/parse/navigate/colon_symbol.rs rename to gix-revision/tests/revision/spec/parse/navigate/colon_symbol.rs diff --git a/gix-revision/tests/spec/parse/navigate/mod.rs b/gix-revision/tests/revision/spec/parse/navigate/mod.rs similarity index 100% rename from gix-revision/tests/spec/parse/navigate/mod.rs rename to gix-revision/tests/revision/spec/parse/navigate/mod.rs diff --git a/gix-revision/tests/spec/parse/navigate/tilde_symbol.rs b/gix-revision/tests/revision/spec/parse/navigate/tilde_symbol.rs similarity index 100% rename from gix-revision/tests/spec/parse/navigate/tilde_symbol.rs rename to gix-revision/tests/revision/spec/parse/navigate/tilde_symbol.rs