Skip to content

Commit

Permalink
feat: add first 'debug' version of gix log
Browse files Browse the repository at this point in the history
It's primarily meant to better understand `gix blame`.
  • Loading branch information
cruessler authored and Byron committed Dec 21, 2024
1 parent df5cead commit c7e04e9
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 0 deletions.
170 changes: 170 additions & 0 deletions gitoxide-core/src/repository/log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use gix::bstr::{BStr, BString, ByteSlice};
use gix::prelude::FindExt;
use gix::ObjectId;

pub fn log(mut repo: gix::Repository, out: &mut dyn std::io::Write, path: Option<BString>) -> anyhow::Result<()> {
repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&**repo.index_or_empty()?));

if let Some(path) = path {
log_file(repo, out, path)
} else {
log_all(repo, out)
}
}

fn log_all(repo: gix::Repository, out: &mut dyn std::io::Write) -> Result<(), anyhow::Error> {
let head = repo.head()?.peel_to_commit_in_place()?;
let topo = gix::traverse::commit::topo::Builder::from_iters(&repo.objects, [head.id], None::<Vec<gix::ObjectId>>)
.build()?;

for info in topo {
let info = info?;

write_info(&repo, &mut *out, &info)?;
}

Ok(())
}

fn log_file(repo: gix::Repository, out: &mut dyn std::io::Write, path: BString) -> anyhow::Result<()> {
let head = repo.head()?.peel_to_commit_in_place()?;
let topo = gix::traverse::commit::topo::Builder::from_iters(&repo.objects, [head.id], None::<Vec<gix::ObjectId>>)
.build()?;

'outer: for info in topo {
let info = info?;
let commit = repo.find_commit(info.id).unwrap();

let tree = repo.find_tree(commit.tree_id().unwrap()).unwrap();

let entry = tree.lookup_entry_by_path(path.to_path().unwrap()).unwrap();

let Some(entry) = entry else {
continue;
};

let parent_ids: Vec<_> = commit.parent_ids().collect();

if parent_ids.is_empty() {
// We confirmed above that the file is in `commit`'s tree. If `parent_ids` is
// empty, the file was added in `commit`.

write_info(&repo, out, &info)?;

break;
}

let parent_ids_with_changes: Vec<_> = parent_ids
.clone()
.into_iter()
.filter(|parent_id| {
let parent_commit = repo.find_commit(*parent_id).unwrap();
let parent_tree = repo.find_tree(parent_commit.tree_id().unwrap()).unwrap();
let parent_entry = parent_tree.lookup_entry_by_path(path.to_path().unwrap()).unwrap();

if let Some(parent_entry) = parent_entry {
if entry.oid() == parent_entry.oid() {
// The blobs storing the file in `entry` and `parent_entry` are
// identical which means the file was not changed in `commit`.

return false;
}
}

true
})
.collect();

if parent_ids.len() != parent_ids_with_changes.len() {
// At least one parent had an identical version of the file which means it was not
// changed in `commit`.

continue;
}

for parent_id in parent_ids_with_changes {
let modifications =
get_modifications_for_file_path(&repo.objects, path.as_ref(), commit.id, parent_id.into());

if !modifications.is_empty() {
write_info(&repo, &mut *out, &info)?;

// We continue because we’ve already determined that this commit is part of the
// file’s history, so there’s no need to compare it to its other parents.

continue 'outer;
}
}
}

Ok(())
}

fn write_info(
repo: &gix::Repository,
mut out: impl std::io::Write,
info: &gix::traverse::commit::Info,
) -> Result<(), std::io::Error> {
let commit = repo.find_commit(info.id).unwrap();

let message = commit.message_raw_sloppy();
let title = message.lines().next();

writeln!(
out,
"{} {}",
info.id.to_hex_with_len(8),
title.map_or_else(|| "<no message>".into(), BString::from)
)?;

Ok(())
}

fn get_modifications_for_file_path(
odb: impl gix::objs::Find + gix::objs::FindHeader,
file_path: &BStr,
id: ObjectId,
parent_id: ObjectId,
) -> Vec<gix::diff::tree::recorder::Change> {
let mut buffer = Vec::new();

let parent = odb.find_commit(&parent_id, &mut buffer).unwrap();

let mut buffer = Vec::new();
let parent_tree_iter = odb
.find(&parent.tree(), &mut buffer)
.unwrap()
.try_into_tree_iter()
.unwrap();

let mut buffer = Vec::new();
let commit = odb.find_commit(&id, &mut buffer).unwrap();

let mut buffer = Vec::new();
let tree_iter = odb
.find(&commit.tree(), &mut buffer)
.unwrap()
.try_into_tree_iter()
.unwrap();

let mut recorder = gix::diff::tree::Recorder::default();
gix::diff::tree(
parent_tree_iter,
tree_iter,
gix::diff::tree::State::default(),
&odb,
&mut recorder,
)
.unwrap();

recorder
.records
.iter()
.filter(|change| match change {
gix::diff::tree::recorder::Change::Modification { path, .. } => path == file_path,
gix::diff::tree::recorder::Change::Addition { path, .. } => path == file_path,
_ => false,
})
.cloned()
.collect()
}
1 change: 1 addition & 0 deletions gitoxide-core/src/repository/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub mod commitgraph;
mod fsck;
pub use fsck::function as fsck;
pub mod index;
pub mod log;
pub mod mailmap;
mod merge_base;
pub use merge_base::merge_base;
Expand Down
9 changes: 9 additions & 0 deletions src/plumbing/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,15 @@ pub fn main() -> Result<()> {
},
),
},
Subcommands::Log(crate::plumbing::options::log::Platform { pathspec }) => prepare_and_run(
"log",
trace,
verbose,
progress,
progress_keep_open,
None,
move |_progress, out, _err| core::repository::log::log(repository(Mode::Lenient)?, out, pathspec),
),
Subcommands::Worktree(crate::plumbing::options::worktree::Platform { cmd }) => match cmd {
crate::plumbing::options::worktree::SubCommands::List => prepare_and_run(
"worktree-list",
Expand Down
13 changes: 13 additions & 0 deletions src/plumbing/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ pub enum Subcommands {
MergeBase(merge_base::Command),
Merge(merge::Platform),
Diff(diff::Platform),
Log(log::Platform),
Worktree(worktree::Platform),
/// Subcommands that need no git repository to run.
#[clap(subcommand)]
Expand Down Expand Up @@ -499,6 +500,18 @@ pub mod diff {
}
}

pub mod log {
use gix::bstr::BString;

/// List all commits in a repository, optionally limited to those that change a given path
#[derive(Debug, clap::Parser)]
pub struct Platform {
/// The git path specification to show a log for.
#[clap(value_parser = crate::shared::AsBString)]
pub pathspec: Option<BString>,
}
}

pub mod config {
use gix::bstr::BString;

Expand Down

0 comments on commit c7e04e9

Please sign in to comment.