History

smart-release: prepare for fast lookup of paths (#198)

Sebastian Thiel committed 5 years ago

Commit fbf267e1 parent d422b9a6 files · +89 −55Browse files

Unified · Split

6 files
  1. cargo-smart-release/src/command/changelog/git.rs +31 −13
  2. cargo-smart-release/src/command/changelog/mod.rs +2 −2
  3. cargo-smart-release/src/command/release/git.rs +3 −3
  4. cargo-smart-release/src/context.rs +19 −3
  5. cargo-smart-release/src/git.rs +26 −34
  6. cargo-smart-release/src/utils.rs +8 −0
cargo-smart-release/src/command/changelog/git.rs+31−13
@@ -1,7 +1,6 @@
11 use std::{collections::BTreeMap, iter::FromIterator, path::PathBuf, time::Instant};
22
33 use anyhow::bail;
4−use cargo_metadata::Metadata;
54 use git_repository as git;
65 use git_repository::{
76 bstr::{BStr, ByteSlice},
@@ -9,7 +8,7 @@ use git_repository::{ · 1 unmodified line
98 prelude::{CacheAccessExt, ObjectAccessExt, ReferenceAccessExt, ReferenceExt},
109 };
1110
12−use crate::utils::{is_tag_name, is_tag_version, package_by_name, tag_prefix};
11+use crate::utils::{component_to_bytes, is_tag_name, is_tag_version, package_by_name, tag_prefix};
1312
1413 /// A head reference will all commits that are 'governed' by it, that is are in its exclusive ancestry.
1514 pub struct Segment<'a> {
@@ -71,15 +70,15 @@ pub fn commit_history(repo: &git::Easy) -> anyhow::Result<Option<History>> { · 55 unmodified lines
7170 /// Return the head reference followed by all tags affecting `crate_name` as per our tag name rules, ordered by ancestry.
7271 pub fn crate_references_descending<'h>(
7372 crate_name: &str,
74− meta: &Metadata,
75− repo: &git::Easy,
73+ ctx: &crate::Context,
7674 history: &'h History,
7775 ) -> anyhow::Result<Vec<Segment<'h>>> {
76+ let meta = &ctx.meta;
7877 let package = package_by_name(meta, crate_name)?;
79− let tag_prefix = tag_prefix(package, repo);
78+ let tag_prefix = tag_prefix(package, &ctx.repo);
8079 let start = Instant::now();
8180 let mut tags_by_commit = {
82− let refs = repo.references()?;
81+ let refs = ctx.repo.references()?;
8382 match tag_prefix {
8483 Some(prefix) => BTreeMap::from_iter(
8584 refs.prefixed(PathBuf::from(format!("refs/tags/{}-", prefix)))?
@@ -106,7 +105,8 @@ pub fn crate_references_descending<'h>( · 20 unmodified lines
106105
107106 let elapsed = start.elapsed();
108107 log::trace!(
109− "Mapped {} tags in {}s ({:.0} refs/s)",
108+ "{}: Mapped {} tags in {}s ({:.0} refs/s)",
109+ crate_name,
110110 tags_by_commit.len(),
111111 elapsed.as_secs_f32(),
112112 tags_by_commit.len() as f32 / elapsed.as_secs_f32()
@@ -118,12 +118,28 @@ pub fn crate_references_descending<'h>( · 5 unmodified lines
118118 _head: history.head.to_owned(),
119119 history: vec![],
120120 };
121+
122+ let dir = ctx.repo_relative_path(package);
123+ enum Filter<'a> {
124+ None,
125+ Fast(&'a [u8]),
126+ }
127+ let filter = dir
128+ .map(|dir| {
129+ let mut components = dir.components().collect::<Vec<_>>();
130+ match components.len() {
131+ 0 => unreachable!("BUG: it's None if empty"),
132+ 1 => Filter::Fast(components.pop().map(component_to_bytes).expect("exactly one")),
133+ _ => todo!("slow mode"),
134+ }
135+ })
136+ .unwrap_or(Filter::None);
121137 for item in &history.items {
122138 match tags_by_commit.remove(&item.id) {
123− None => {
124− // TODO: filter by touched panifest path, fast or slow path
125− segment.history.push(item)
126− }
139+ None => match filter {
140+ Filter::None => segment.history.push(item),
141+ Filter::Fast(_comp) => todo!("fast lookup"),
142+ },
127143 Some(next_ref) => segments.push(std::mem::replace(
128144 &mut segment,
129145 Segment {
@@ -137,7 +153,8 @@ pub fn crate_references_descending<'h>( · 7 unmodified lines
137153
138154 if !tags_by_commit.is_empty() {
139155 log::warn!(
140− "The following tags were on branches which are ignored during traversal: {}",
156+ "{}: The following tags were on branches which are ignored during traversal: {}",
157+ crate_name,
141158 tags_by_commit
142159 .into_values()
143160 .map(|v| v.name.as_bstr().to_str_lossy().into_owned())
@@ -149,7 +166,8 @@ pub fn crate_references_descending<'h>( · 5 unmodified lines
149166 let elapsed = start.elapsed();
150167 let num_commits = segments.iter().map(|s| s.history.len()).sum::<usize>();
151168 log::trace!(
152− "Found {} relevant commits out of {} in {} segments {}s ({:.0} commits/s)",
169+ "{}: Found {} relevant commits out of {} in {} segments {}s ({:.0} commits/s)",
170+ crate_name,
153171 num_commits,
154172 history.items.len(),
155173 segments.len(),
cargo-smart-release/src/command/changelog/mod.rs+2−2
@@ -10,12 +10,12 @@ pub fn changelog(options: Options, crates: Vec<String>) -> anyhow::Result<()> { · 9 unmodified lines
1010 ctx.crate_names.clone()
1111 };
1212 assure_working_tree_is_unchanged(options)?;
13− let history = match git::commit_history(&ctx.git_easy)? {
13+ let history = match git::commit_history(&ctx.repo)? {
1414 None => return Ok(()),
1515 Some(history) => history,
1616 };
1717 for crate_name in &crate_names {
18− git::crate_references_descending(crate_name, &ctx.meta, &ctx.git_easy, &history)?;
18+ git::crate_references_descending(crate_name, &ctx, &history)?;
1919 }
2020
2121 Ok(())
cargo-smart-release/src/command/release/git.rs+3−3
@@ -31,7 +31,7 @@ pub(in crate::command::release_impl) fn commit_changes( · 30 unmodified lines
3131 if !cmd.status()?.success() {
3232 bail!("Failed to commit changed manifests");
3333 }
34− Ok(Some(ctx.git_easy.find_reference("HEAD")?.peel_to_id_in_place()?))
34+ Ok(Some(ctx.repo.find_reference("HEAD")?.peel_to_id_in_place()?))
3535 }
3636
3737 pub(in crate::command::release_impl) fn create_version_tag<'repo>(
@@ -49,7 +49,7 @@ pub(in crate::command::release_impl) fn create_version_tag<'repo>( · 11 unmodified lines
4949 if skip_tag {
5050 return Ok(None);
5151 }
52− let tag_name = tag_name(publishee, new_version, &ctx.git_easy);
52+ let tag_name = tag_name(publishee, new_version, &ctx.repo);
5353 if dry_run {
5454 if verbose {
5555 log::info!("WOULD create tag {}", tag_name);
@@ -57,7 +57,7 @@ pub(in crate::command::release_impl) fn create_version_tag<'repo>( · 1 unmodified line
5757 Ok(Some(format!("refs/tags/{}", tag_name).try_into()?))
5858 } else {
5959 let tag = ctx
60− .git_easy
60+ .repo
6161 .tag(tag_name, commit_id.expect("set in --execute mode"), PreviousValue::Any)?;
6262 log::info!("Created tag {}", tag.name().as_bstr());
6363 Ok(Some(tag.inner.name))
cargo-smart-release/src/context.rs+19−3
@@ -1,11 +1,12 @@
1−use cargo_metadata::{camino::Utf8PathBuf, Metadata};
1+use cargo_metadata::camino::Utf8Path;
2+use cargo_metadata::{camino::Utf8PathBuf, Metadata, Package};
23 use git_repository as git;
34 use git_repository::prelude::CacheAccessExt;
45
56 pub struct Context {
67 pub root: Utf8PathBuf,
78 pub meta: Metadata,
8− pub git_easy: git::Easy,
9+ pub repo: git::Easy,
910 pub crate_names: Vec<String>,
1011 }
1112
@@ -16,11 +17,26 @@ impl Context { · 4 unmodified lines
1617 let repo = git::discover(&root)?;
1718 Ok(Context {
1819 root,
19− git_easy: repo.into_easy().apply_environment()?,
20+ repo: repo.into_easy().apply_environment()?,
2021 meta,
2122 crate_names: fill_in_root_crate_if_needed(crate_names)?,
2223 })
2324 }
25+
26+ pub(crate) fn repo_relative_path<'a>(&self, p: &'a Package) -> Option<&'a Utf8Path> {
27+ let dir = p
28+ .manifest_path
29+ .parent()
30+ .expect("parent of a file is always present")
31+ .strip_prefix(&self.root)
32+ .expect("workspace members are releative to the root directory");
33+
34+ if dir.as_os_str().is_empty() {
35+ None
36+ } else {
37+ dir.into()
38+ }
39+ }
2440 }
2541
2642 fn fill_in_root_crate_if_needed(crate_names: Vec<String>) -> anyhow::Result<Vec<String>> {
cargo-smart-release/src/git.rs+26−34
@@ -2,14 +2,14 @@ use std::process::Command; · 1 unmodified line
22
33 use anyhow::{anyhow, bail};
44 use bstr::ByteSlice;
5−use cargo_metadata::{camino::Utf8Component, Package};
5+use cargo_metadata::Package;
66 use git_repository::{easy::object, prelude::ReferenceAccessExt};
77
8−use crate::utils::tag_name;
8+use crate::utils::{component_to_bytes, tag_name};
99
1010 pub fn has_changed_since_last_release(package: &Package, ctx: &crate::Context, verbose: bool) -> anyhow::Result<bool> {
11− let version_tag_name = tag_name(package, &package.version.to_string(), &ctx.git_easy);
12− let mut tag_ref = match ctx.git_easy.try_find_reference(&version_tag_name)? {
11+ let version_tag_name = tag_name(package, &package.version.to_string(), &ctx.repo);
12+ let mut tag_ref = match ctx.repo.try_find_reference(&version_tag_name)? {
1313 None => {
1414 if verbose {
1515 log::info!(
@@ -22,41 +22,33 @@ pub fn has_changed_since_last_release(package: &Package, ctx: &crate::Context, v · 6 unmodified lines
2222 }
2323 Some(r) => r,
2424 };
25− let repo_relative_crate_dir = package
26− .manifest_path
27− .parent()
28− .expect("parent of a file is always present")
29− .strip_prefix(&ctx.root)
30− .expect("workspace members are releative to the root directory");
31−
32− Ok(match ctx.git_easy.head()?.into_fully_peeled_id() {
25+ let repo_relative_crate_dir = ctx.repo_relative_path(package);
26+ Ok(match ctx.repo.head()?.into_fully_peeled_id() {
3327 Some(c) => {
3428 let current_commit = c?;
3529 let released_target = tag_ref.peel_to_id_in_place()?;
3630
37− if repo_relative_crate_dir.as_os_str().is_empty() {
38− current_commit != released_target
39− } else {
40− let components = repo_relative_crate_dir.components().map(|c| match c {
41− Utf8Component::Normal(c) => c.as_bytes(),
42− _ => unreachable!("only normal components are possible in paths here"),
43− });
44− let current_dir_id = current_commit
45− .object()?
46− .peel_to_kind(object::Kind::Tree)?
47− .into_tree()
48− .lookup_path(components.clone())?
49− .expect("path must exist in current commit")
50− .oid;
51− let released_dir_id = released_target
52− .object()?
53− .peel_to_kind(object::Kind::Tree)?
54− .into_tree()
55− .lookup_path(components)?
56− .expect("path must exist as it was supposedly released there")
57− .oid;
31+ match repo_relative_crate_dir {
32+ None => current_commit != released_target,
33+ Some(dir) => {
34+ let components = dir.components().map(component_to_bytes);
35+ let current_dir_id = current_commit
36+ .object()?
37+ .peel_to_kind(object::Kind::Tree)?
38+ .into_tree()
39+ .lookup_path(components.clone())?
40+ .expect("path must exist in current commit")
41+ .oid;
42+ let released_dir_id = released_target
43+ .object()?
44+ .peel_to_kind(object::Kind::Tree)?
45+ .into_tree()
46+ .lookup_path(components)?
47+ .expect("path must exist as it was supposedly released there")
48+ .oid;
5849
59− released_dir_id != current_dir_id
50+ released_dir_id != current_dir_id
51+ }
6052 }
6153 }
6254 None => true,
cargo-smart-release/src/utils.rs+8−0
@@ -1,4 +1,5 @@
11 use anyhow::anyhow;
2+use cargo_metadata::camino::Utf8Component;
23 use cargo_metadata::{camino::Utf8Path, Dependency, Metadata, Package, PackageId};
34 use git_repository as git;
45 use semver::Version;
@@ -111,6 +112,13 @@ pub fn is_tag_version(name: &git::bstr::BStr) -> bool { · 106 unmodified lines
111112 name.starts_with_str(b"v") && name.split_str(b".").count() >= 3
112113 }
113114
115+pub fn component_to_bytes(c: Utf8Component<'_>) -> &[u8] {
116+ match c {
117+ Utf8Component::Normal(c) => c.as_bytes(),
118+ _ => unreachable!("only normal components are possible in paths here"),
119+ }
120+}
121+
114122 #[cfg(test)]
115123 mod tests {
116124 mod is_tag_name {