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 @@
1use std::{collections::BTreeMap, iter::FromIterator, path::PathBuf, time::Instant};1use std::{collections::BTreeMap, iter::FromIterator, path::PathBuf, time::Instant};
2 2
3use anyhow::bail;3use anyhow::bail;
4use cargo_metadata::Metadata;
5use git_repository as git;4use git_repository as git;
6use git_repository::{5use git_repository::{
7 bstr::{BStr, ByteSlice},6 bstr::{BStr, ByteSlice},
@@ -9,7 +8,7 @@ use git_repository::{ · 1 unmodified line
9 prelude::{CacheAccessExt, ObjectAccessExt, ReferenceAccessExt, ReferenceExt},8 prelude::{CacheAccessExt, ObjectAccessExt, ReferenceAccessExt, ReferenceExt},
10};9};
11 10
12use crate::utils::{is_tag_name, is_tag_version, package_by_name, tag_prefix};11use crate::utils::{component_to_bytes, is_tag_name, is_tag_version, package_by_name, tag_prefix};
13 12
14/// A head reference will all commits that are 'governed' by it, that is are in its exclusive ancestry.13/// A head reference will all commits that are 'governed' by it, that is are in its exclusive ancestry.
15pub struct Segment<'a> {14pub struct Segment<'a> {
@@ -71,15 +70,15 @@ pub fn commit_history(repo: &git::Easy) -> anyhow::Result<Option<History>> { · 55 unmodified lines
71/// Return the head reference followed by all tags affecting `crate_name` as per our tag name rules, ordered by ancestry.70/// Return the head reference followed by all tags affecting `crate_name` as per our tag name rules, ordered by ancestry.
72pub fn crate_references_descending<'h>(71pub fn crate_references_descending<'h>(
73 crate_name: &str,72 crate_name: &str,
74 meta: &Metadata,73 ctx: &crate::Context,
75 repo: &git::Easy,
76 history: &'h History,74 history: &'h History,
77) -> anyhow::Result<Vec<Segment<'h>>> {75) -> anyhow::Result<Vec<Segment<'h>>> {
76 let meta = &ctx.meta;
78 let package = package_by_name(meta, crate_name)?;77 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);
80 let start = Instant::now();79 let start = Instant::now();
81 let mut tags_by_commit = {80 let mut tags_by_commit = {
82 let refs = repo.references()?;81 let refs = ctx.repo.references()?;
83 match tag_prefix {82 match tag_prefix {
84 Some(prefix) => BTreeMap::from_iter(83 Some(prefix) => BTreeMap::from_iter(
85 refs.prefixed(PathBuf::from(format!("refs/tags/{}-", prefix)))?84 refs.prefixed(PathBuf::from(format!("refs/tags/{}-", prefix)))?
@@ -106,7 +105,8 @@ pub fn crate_references_descending<'h>( · 20 unmodified lines
106 105
107 let elapsed = start.elapsed();106 let elapsed = start.elapsed();
108 log::trace!(107 log::trace!(
109 "Mapped {} tags in {}s ({:.0} refs/s)",108 "{}: Mapped {} tags in {}s ({:.0} refs/s)",
109 crate_name,
110 tags_by_commit.len(),110 tags_by_commit.len(),
111 elapsed.as_secs_f32(),111 elapsed.as_secs_f32(),
112 tags_by_commit.len() as f32 / elapsed.as_secs_f32()112 tags_by_commit.len() as f32 / elapsed.as_secs_f32()
@@ -118,12 +118,28 @@ pub fn crate_references_descending<'h>( · 5 unmodified lines
118 _head: history.head.to_owned(),118 _head: history.head.to_owned(),
119 history: vec![],119 history: vec![],
120 };120 };
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);
121 for item in &history.items {137 for item in &history.items {
122 match tags_by_commit.remove(&item.id) {138 match tags_by_commit.remove(&item.id) {
123 None => {139 None => match filter {
124 // TODO: filter by touched panifest path, fast or slow path140 Filter::None => segment.history.push(item),
125 segment.history.push(item)141 Filter::Fast(_comp) => todo!("fast lookup"),
126 }142 },
127 Some(next_ref) => segments.push(std::mem::replace(143 Some(next_ref) => segments.push(std::mem::replace(
128 &mut segment,144 &mut segment,
129 Segment {145 Segment {
@@ -137,7 +153,8 @@ pub fn crate_references_descending<'h>( · 7 unmodified lines
137 153
138 if !tags_by_commit.is_empty() {154 if !tags_by_commit.is_empty() {
139 log::warn!(155 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,
141 tags_by_commit158 tags_by_commit
142 .into_values()159 .into_values()
143 .map(|v| v.name.as_bstr().to_str_lossy().into_owned())160 .map(|v| v.name.as_bstr().to_str_lossy().into_owned())
@@ -149,7 +166,8 @@ pub fn crate_references_descending<'h>( · 5 unmodified lines
149 let elapsed = start.elapsed();166 let elapsed = start.elapsed();
150 let num_commits = segments.iter().map(|s| s.history.len()).sum::<usize>();167 let num_commits = segments.iter().map(|s| s.history.len()).sum::<usize>();
151 log::trace!(168 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,
153 num_commits,171 num_commits,
154 history.items.len(),172 history.items.len(),
155 segments.len(),173 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
10 ctx.crate_names.clone()10 ctx.crate_names.clone()
11 };11 };
12 assure_working_tree_is_unchanged(options)?;12 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)? {
14 None => return Ok(()),14 None => return Ok(()),
15 Some(history) => history,15 Some(history) => history,
16 };16 };
17 for crate_name in &crate_names {17 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)?;
19 }19 }
20 20
21 Ok(())21 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
31 if !cmd.status()?.success() {31 if !cmd.status()?.success() {
32 bail!("Failed to commit changed manifests");32 bail!("Failed to commit changed manifests");
33 }33 }
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()?))
35}35}
36 36
37pub(in crate::command::release_impl) fn create_version_tag<'repo>(37pub(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
49 if skip_tag {49 if skip_tag {
50 return Ok(None);50 return Ok(None);
51 }51 }
52 let tag_name = tag_name(publishee, new_version, &ctx.git_easy);52 let tag_name = tag_name(publishee, new_version, &ctx.repo);
53 if dry_run {53 if dry_run {
54 if verbose {54 if verbose {
55 log::info!("WOULD create tag {}", tag_name);55 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
57 Ok(Some(format!("refs/tags/{}", tag_name).try_into()?))57 Ok(Some(format!("refs/tags/{}", tag_name).try_into()?))
58 } else {58 } else {
59 let tag = ctx59 let tag = ctx
60 .git_easy60 .repo
61 .tag(tag_name, commit_id.expect("set in --execute mode"), PreviousValue::Any)?;61 .tag(tag_name, commit_id.expect("set in --execute mode"), PreviousValue::Any)?;
62 log::info!("Created tag {}", tag.name().as_bstr());62 log::info!("Created tag {}", tag.name().as_bstr());
63 Ok(Some(tag.inner.name))63 Ok(Some(tag.inner.name))
cargo-smart-release/src/context.rs+19−3
@@ -1,11 +1,12 @@
1use cargo_metadata::{camino::Utf8PathBuf, Metadata};1use cargo_metadata::camino::Utf8Path;
2use cargo_metadata::{camino::Utf8PathBuf, Metadata, Package};
2use git_repository as git;3use git_repository as git;
3use git_repository::prelude::CacheAccessExt;4use git_repository::prelude::CacheAccessExt;
4 5
5pub struct Context {6pub struct Context {
6 pub root: Utf8PathBuf,7 pub root: Utf8PathBuf,
7 pub meta: Metadata,8 pub meta: Metadata,
8 pub git_easy: git::Easy,9 pub repo: git::Easy,
9 pub crate_names: Vec<String>,10 pub crate_names: Vec<String>,
10}11}
11 12
@@ -16,11 +17,26 @@ impl Context { · 4 unmodified lines
16 let repo = git::discover(&root)?;17 let repo = git::discover(&root)?;
17 Ok(Context {18 Ok(Context {
18 root,19 root,
19 git_easy: repo.into_easy().apply_environment()?,20 repo: repo.into_easy().apply_environment()?,
20 meta,21 meta,
21 crate_names: fill_in_root_crate_if_needed(crate_names)?,22 crate_names: fill_in_root_crate_if_needed(crate_names)?,
22 })23 })
23 }24 }
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 }
24}40}
25 41
26fn fill_in_root_crate_if_needed(crate_names: Vec<String>) -> anyhow::Result<Vec<String>> {42fn 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
2 2
3use anyhow::{anyhow, bail};3use anyhow::{anyhow, bail};
4use bstr::ByteSlice;4use bstr::ByteSlice;
5use cargo_metadata::{camino::Utf8Component, Package};5use cargo_metadata::Package;
6use git_repository::{easy::object, prelude::ReferenceAccessExt};6use git_repository::{easy::object, prelude::ReferenceAccessExt};
7 7
8use crate::utils::tag_name;8use crate::utils::{component_to_bytes, tag_name};
9 9
10pub fn has_changed_since_last_release(package: &Package, ctx: &crate::Context, verbose: bool) -> anyhow::Result<bool> {10pub 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);11 let version_tag_name = tag_name(package, &package.version.to_string(), &ctx.repo);
12 let mut tag_ref = match ctx.git_easy.try_find_reference(&version_tag_name)? {12 let mut tag_ref = match ctx.repo.try_find_reference(&version_tag_name)? {
13 None => {13 None => {
14 if verbose {14 if verbose {
15 log::info!(15 log::info!(
@@ -22,41 +22,33 @@ pub fn has_changed_since_last_release(package: &Package, ctx: &crate::Context, v · 6 unmodified lines
22 }22 }
23 Some(r) => r,23 Some(r) => r,
24 };24 };
25 let repo_relative_crate_dir = package25 let repo_relative_crate_dir = ctx.repo_relative_path(package);
26 .manifest_path26 Ok(match ctx.repo.head()?.into_fully_peeled_id() {
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() {
33 Some(c) => {27 Some(c) => {
34 let current_commit = c?;28 let current_commit = c?;
35 let released_target = tag_ref.peel_to_id_in_place()?;29 let released_target = tag_ref.peel_to_id_in_place()?;
36 30
37 if repo_relative_crate_dir.as_os_str().is_empty() {31 match repo_relative_crate_dir {
38 current_commit != released_target32 None => current_commit != released_target,
39 } else {33 Some(dir) => {
40 let components = repo_relative_crate_dir.components().map(|c| match c {34 let components = dir.components().map(component_to_bytes);
41 Utf8Component::Normal(c) => c.as_bytes(),35 let current_dir_id = current_commit
42 _ => unreachable!("only normal components are possible in paths here"),36 .object()?
43 });37 .peel_to_kind(object::Kind::Tree)?
44 let current_dir_id = current_commit38 .into_tree()
45 .object()?39 .lookup_path(components.clone())?
46 .peel_to_kind(object::Kind::Tree)?40 .expect("path must exist in current commit")
47 .into_tree()41 .oid;
48 .lookup_path(components.clone())?42 let released_dir_id = released_target
49 .expect("path must exist in current commit")43 .object()?
50 .oid;44 .peel_to_kind(object::Kind::Tree)?
51 let released_dir_id = released_target45 .into_tree()
52 .object()?46 .lookup_path(components)?
53 .peel_to_kind(object::Kind::Tree)?47 .expect("path must exist as it was supposedly released there")
54 .into_tree()48 .oid;
55 .lookup_path(components)?
56 .expect("path must exist as it was supposedly released there")
57 .oid;
58 49
59 released_dir_id != current_dir_id50 released_dir_id != current_dir_id
51 }
60 }52 }
61 }53 }
62 None => true,54 None => true,
cargo-smart-release/src/utils.rs+8−0
@@ -1,4 +1,5 @@
1use anyhow::anyhow;1use anyhow::anyhow;
2use cargo_metadata::camino::Utf8Component;
2use cargo_metadata::{camino::Utf8Path, Dependency, Metadata, Package, PackageId};3use cargo_metadata::{camino::Utf8Path, Dependency, Metadata, Package, PackageId};
3use git_repository as git;4use git_repository as git;
4use semver::Version;5use semver::Version;
@@ -111,6 +112,13 @@ pub fn is_tag_version(name: &git::bstr::BStr) -> bool { · 106 unmodified lines
111 name.starts_with_str(b"v") && name.split_str(b".").count() >= 3112 name.starts_with_str(b"v") && name.split_str(b".").count() >= 3
112}113}
113 114
115pub 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
114#[cfg(test)]122#[cfg(test)]
115mod tests {123mod tests {
116 mod is_tag_name {124 mod is_tag_name {