Unified · Split
| @@ -1,7 +1,6 @@ | |||
| 1 | use std::{collections::BTreeMap, iter::FromIterator, path::PathBuf, time::Instant}; | 1 | use std::{collections::BTreeMap, iter::FromIterator, path::PathBuf, time::Instant}; |
| 2 | 2 | ||
| 3 | use anyhow::bail; | 3 | use anyhow::bail; |
| 4 | use cargo_metadata::Metadata; | ||
| 5 | use git_repository as git; | 4 | use git_repository as git; |
| 6 | use git_repository::{ | 5 | use 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 | ||
| 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 | /// 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. |
| 15 | pub struct Segment<'a> { | 14 | pub 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. |
| 72 | pub fn crate_references_descending<'h>( | 71 | pub 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 path | 140 | 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_commit | 158 | 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(), |
| @@ -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(()) |
| @@ -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 | ||
| 37 | pub(in crate::command::release_impl) fn create_version_tag<'repo>( | 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 | 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 = ctx | 59 | let tag = ctx |
| 60 | .git_easy | 60 | .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)) |
| @@ -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 | use git_repository as git; | 3 | use git_repository as git; |
| 3 | use git_repository::prelude::CacheAccessExt; | 4 | use git_repository::prelude::CacheAccessExt; |
| 4 | 5 | ||
| 5 | pub struct Context { | 6 | pub 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 | ||
| 26 | fn fill_in_root_crate_if_needed(crate_names: Vec<String>) -> anyhow::Result<Vec<String>> { | 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 | use anyhow::{anyhow, bail}; | 3 | use anyhow::{anyhow, bail}; |
| 4 | use bstr::ByteSlice; | 4 | use bstr::ByteSlice; |
| 5 | use cargo_metadata::{camino::Utf8Component, Package}; | 5 | use cargo_metadata::Package; |
| 6 | use git_repository::{easy::object, prelude::ReferenceAccessExt}; | 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 | pub fn has_changed_since_last_release(package: &Package, ctx: &crate::Context, verbose: bool) -> anyhow::Result<bool> { | 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); | 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 = package | 25 | let repo_relative_crate_dir = ctx.repo_relative_path(package); |
| 26 | .manifest_path | 26 | 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_target | 32 | 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_commit | 38 | .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_target | 45 | .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_id | 50 | released_dir_id != current_dir_id |
| 51 | } | ||
| 60 | } | 52 | } |
| 61 | } | 53 | } |
| 62 | None => true, | 54 | None => true, |
| @@ -1,4 +1,5 @@ | |||
| 1 | use anyhow::anyhow; | 1 | use anyhow::anyhow; |
| 2 | use cargo_metadata::camino::Utf8Component; | ||
| 2 | use cargo_metadata::{camino::Utf8Path, Dependency, Metadata, Package, PackageId}; | 3 | use cargo_metadata::{camino::Utf8Path, Dependency, Metadata, Package, PackageId}; |
| 3 | use git_repository as git; | 4 | use git_repository as git; |
| 4 | use semver::Version; | 5 | use 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() >= 3 | 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 | #[cfg(test)] | 122 | #[cfg(test)] |
| 115 | mod tests { | 123 | mod tests { |
| 116 | mod is_tag_name { | 124 | mod is_tag_name { |