Unified · Split
| @@ -1,7 +1,6 @@ | ||
| 1 | 1 | use std::{collections::BTreeMap, iter::FromIterator, path::PathBuf, time::Instant}; |
| 2 | 2 | |
| 3 | 3 | use anyhow::bail; |
| 4 | −use cargo_metadata::Metadata; | |
| 5 | 4 | use git_repository as git; |
| 6 | 5 | use git_repository::{ |
| 7 | 6 | bstr::{BStr, ByteSlice}, |
| @@ -9,7 +8,7 @@ use git_repository::{ · 1 unmodified line | ||
| 9 | 8 | prelude::{CacheAccessExt, ObjectAccessExt, ReferenceAccessExt, ReferenceExt}, |
| 10 | 9 | }; |
| 11 | 10 | |
| 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}; | |
| 13 | 12 | |
| 14 | 13 | /// A head reference will all commits that are 'governed' by it, that is are in its exclusive ancestry. |
| 15 | 14 | pub struct Segment<'a> { |
| @@ -71,15 +70,15 @@ pub fn commit_history(repo: &git::Easy) -> anyhow::Result<Option<History>> { · 55 unmodified lines | ||
| 71 | 70 | /// Return the head reference followed by all tags affecting `crate_name` as per our tag name rules, ordered by ancestry. |
| 72 | 71 | pub fn crate_references_descending<'h>( |
| 73 | 72 | crate_name: &str, |
| 74 | − meta: &Metadata, | |
| 75 | − repo: &git::Easy, | |
| 73 | + ctx: &crate::Context, | |
| 76 | 74 | history: &'h History, |
| 77 | 75 | ) -> anyhow::Result<Vec<Segment<'h>>> { |
| 76 | + let meta = &ctx.meta; | |
| 78 | 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 | 79 | let start = Instant::now(); |
| 81 | 80 | let mut tags_by_commit = { |
| 82 | − let refs = repo.references()?; | |
| 81 | + let refs = ctx.repo.references()?; | |
| 83 | 82 | match tag_prefix { |
| 84 | 83 | Some(prefix) => BTreeMap::from_iter( |
| 85 | 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 | 106 | let elapsed = start.elapsed(); |
| 108 | 107 | log::trace!( |
| 109 | − "Mapped {} tags in {}s ({:.0} refs/s)", | |
| 108 | + "{}: Mapped {} tags in {}s ({:.0} refs/s)", | |
| 109 | + crate_name, | |
| 110 | 110 | tags_by_commit.len(), |
| 111 | 111 | elapsed.as_secs_f32(), |
| 112 | 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 | 118 | _head: history.head.to_owned(), |
| 119 | 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 | 137 | for item in &history.items { |
| 122 | 138 | 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 | + }, | |
| 127 | 143 | Some(next_ref) => segments.push(std::mem::replace( |
| 128 | 144 | &mut segment, |
| 129 | 145 | Segment { |
| @@ -137,7 +153,8 @@ pub fn crate_references_descending<'h>( · 7 unmodified lines | ||
| 137 | 153 | |
| 138 | 154 | if !tags_by_commit.is_empty() { |
| 139 | 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 | 158 | tags_by_commit |
| 142 | 159 | .into_values() |
| 143 | 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 | 166 | let elapsed = start.elapsed(); |
| 150 | 167 | let num_commits = segments.iter().map(|s| s.history.len()).sum::<usize>(); |
| 151 | 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 | 171 | num_commits, |
| 154 | 172 | history.items.len(), |
| 155 | 173 | segments.len(), |
| @@ -10,12 +10,12 @@ pub fn changelog(options: Options, crates: Vec<String>) -> anyhow::Result<()> { · 9 unmodified lines | ||
| 10 | 10 | ctx.crate_names.clone() |
| 11 | 11 | }; |
| 12 | 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 | 14 | None => return Ok(()), |
| 15 | 15 | Some(history) => history, |
| 16 | 16 | }; |
| 17 | 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 | 21 | Ok(()) |
| @@ -31,7 +31,7 @@ pub(in crate::command::release_impl) fn commit_changes( · 30 unmodified lines | ||
| 31 | 31 | if !cmd.status()?.success() { |
| 32 | 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 | |
| 37 | 37 | 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 | ||
| 49 | 49 | if skip_tag { |
| 50 | 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 | 53 | if dry_run { |
| 54 | 54 | if verbose { |
| 55 | 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 | 57 | Ok(Some(format!("refs/tags/{}", tag_name).try_into()?)) |
| 58 | 58 | } else { |
| 59 | 59 | let tag = ctx |
| 60 | − .git_easy | |
| 60 | + .repo | |
| 61 | 61 | .tag(tag_name, commit_id.expect("set in --execute mode"), PreviousValue::Any)?; |
| 62 | 62 | log::info!("Created tag {}", tag.name().as_bstr()); |
| 63 | 63 | Ok(Some(tag.inner.name)) |
| @@ -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}; | |
| 2 | 3 | use git_repository as git; |
| 3 | 4 | use git_repository::prelude::CacheAccessExt; |
| 4 | 5 | |
| 5 | 6 | pub struct Context { |
| 6 | 7 | pub root: Utf8PathBuf, |
| 7 | 8 | pub meta: Metadata, |
| 8 | − pub git_easy: git::Easy, | |
| 9 | + pub repo: git::Easy, | |
| 9 | 10 | pub crate_names: Vec<String>, |
| 10 | 11 | } |
| 11 | 12 | |
| @@ -16,11 +17,26 @@ impl Context { · 4 unmodified lines | ||
| 16 | 17 | let repo = git::discover(&root)?; |
| 17 | 18 | Ok(Context { |
| 18 | 19 | root, |
| 19 | − git_easy: repo.into_easy().apply_environment()?, | |
| 20 | + repo: repo.into_easy().apply_environment()?, | |
| 20 | 21 | meta, |
| 21 | 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 | |
| 26 | 42 | fn fill_in_root_crate_if_needed(crate_names: Vec<String>) -> anyhow::Result<Vec<String>> { |
| @@ -2,14 +2,14 @@ use std::process::Command; · 1 unmodified line | ||
| 2 | 2 | |
| 3 | 3 | use anyhow::{anyhow, bail}; |
| 4 | 4 | use bstr::ByteSlice; |
| 5 | −use cargo_metadata::{camino::Utf8Component, Package}; | |
| 5 | +use cargo_metadata::Package; | |
| 6 | 6 | use git_repository::{easy::object, prelude::ReferenceAccessExt}; |
| 7 | 7 | |
| 8 | −use crate::utils::tag_name; | |
| 8 | +use crate::utils::{component_to_bytes, tag_name}; | |
| 9 | 9 | |
| 10 | 10 | 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)? { | |
| 13 | 13 | None => { |
| 14 | 14 | if verbose { |
| 15 | 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 | 23 | Some(r) => r, |
| 24 | 24 | }; |
| 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() { | |
| 33 | 27 | Some(c) => { |
| 34 | 28 | let current_commit = c?; |
| 35 | 29 | let released_target = tag_ref.peel_to_id_in_place()?; |
| 36 | 30 | |
| 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; | |
| 58 | 49 | |
| 59 | − released_dir_id != current_dir_id | |
| 50 | + released_dir_id != current_dir_id | |
| 51 | + } | |
| 60 | 52 | } |
| 61 | 53 | } |
| 62 | 54 | None => true, |
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | use anyhow::anyhow; |
| 2 | +use cargo_metadata::camino::Utf8Component; | |
| 2 | 3 | use cargo_metadata::{camino::Utf8Path, Dependency, Metadata, Package, PackageId}; |
| 3 | 4 | use git_repository as git; |
| 4 | 5 | use semver::Version; |
| @@ -111,6 +112,13 @@ pub fn is_tag_version(name: &git::bstr::BStr) -> bool { · 106 unmodified lines | ||
| 111 | 112 | name.starts_with_str(b"v") && name.split_str(b".").count() >= 3 |
| 112 | 113 | } |
| 113 | 114 | |
| 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 | + | |
| 114 | 122 | #[cfg(test)] |
| 115 | 123 | mod tests { |
| 116 | 124 | mod is_tag_name { |