Skip to content

Commit 7af598e

Browse files
committed
feat: add first 'debug' version of gix log
It's primarily meant to better understand `gix blame`.
1 parent db5c9cf commit 7af598e

File tree

4 files changed

+175
-0
lines changed

4 files changed

+175
-0
lines changed

gitoxide-core/src/repository/log.rs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
use gix::bstr::{BStr, BString, ByteSlice};
2+
use gix::prelude::FindExt;
3+
use gix::ObjectId;
4+
5+
pub fn log(mut repo: gix::Repository, out: &mut dyn std::io::Write, pathspec: BString) -> anyhow::Result<()> {
6+
repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&**repo.index_or_empty()?));
7+
8+
let head = repo.head()?.peel_to_commit_in_place()?;
9+
let infos: Vec<_> =
10+
gix::traverse::commit::topo::Builder::from_iters(&repo.objects, [head.id], None::<Vec<gix::ObjectId>>)
11+
.build()?
12+
.collect();
13+
14+
let infos: Vec<_> = infos
15+
.into_iter()
16+
.filter(|info| {
17+
let commit = repo.find_commit(info.as_ref().unwrap().id).unwrap();
18+
19+
let mut buffer = Vec::new();
20+
let tree = repo.objects.find_tree(&commit.tree_id().unwrap(), &mut buffer).unwrap();
21+
22+
let Some(entry) = tree.bisect_entry(pathspec.as_ref(), false) else {
23+
return false;
24+
};
25+
26+
let parent_ids: Vec<_> = commit.parent_ids().collect();
27+
28+
if parent_ids.is_empty() {
29+
// We confirmed above that the file is in `commit`'s tree. If `parent_ids` is
30+
// empty, the file was added in `commit`.
31+
32+
return true;
33+
}
34+
35+
let parent_ids_with_changes: Vec<_> = parent_ids
36+
.clone()
37+
.into_iter()
38+
.filter(|parent_id| {
39+
let mut buffer = Vec::new();
40+
let parent_commit = repo.find_commit(*parent_id).unwrap();
41+
let parent_tree = repo
42+
.objects
43+
.find_tree(&parent_commit.tree_id().unwrap(), &mut buffer)
44+
.unwrap();
45+
46+
if let Some(parent_entry) = parent_tree.bisect_entry(pathspec.as_ref(), false) {
47+
if entry.oid == parent_entry.oid {
48+
// The blobs storing the file in `entry` and `parent_entry` are
49+
// identical which means the file was not changed in `commit`.
50+
51+
return false;
52+
}
53+
}
54+
55+
true
56+
})
57+
.collect();
58+
59+
if parent_ids.len() != parent_ids_with_changes.len() {
60+
// At least one parent had an identical version of the file which means it was not
61+
// changed in `commit`.
62+
63+
return false;
64+
}
65+
66+
for parent_id in parent_ids_with_changes {
67+
let modifications =
68+
get_modifications_for_file_path(&repo.objects, pathspec.as_ref(), commit.id, parent_id.into());
69+
70+
return !modifications.is_empty();
71+
}
72+
73+
return false;
74+
})
75+
.collect();
76+
77+
write_infos(&repo, out, infos)?;
78+
79+
Ok(())
80+
}
81+
82+
fn write_infos(
83+
repo: &gix::Repository,
84+
mut out: impl std::io::Write,
85+
infos: Vec<Result<gix::traverse::commit::Info, gix::traverse::commit::topo::Error>>,
86+
) -> Result<(), std::io::Error> {
87+
for info in infos {
88+
let info = info.unwrap();
89+
let commit = repo.find_commit(info.id).unwrap();
90+
91+
let message = commit.message_raw_sloppy();
92+
let title = message.lines().next();
93+
94+
writeln!(
95+
out,
96+
"{} {}",
97+
info.id.to_hex_with_len(8),
98+
title.map(BString::from).unwrap_or_else(|| "<no message>".into())
99+
)?;
100+
}
101+
102+
Ok(())
103+
}
104+
105+
fn get_modifications_for_file_path(
106+
odb: impl gix::objs::Find + gix::objs::FindHeader,
107+
file_path: &BStr,
108+
id: ObjectId,
109+
parent_id: ObjectId,
110+
) -> Vec<gix::diff::tree::recorder::Change> {
111+
let mut buffer = Vec::new();
112+
113+
let parent = odb.find_commit(&parent_id, &mut buffer).unwrap();
114+
115+
let mut buffer = Vec::new();
116+
let parent_tree_iter = odb
117+
.find(&parent.tree(), &mut buffer)
118+
.unwrap()
119+
.try_into_tree_iter()
120+
.unwrap();
121+
122+
let mut buffer = Vec::new();
123+
let commit = odb.find_commit(&id, &mut buffer).unwrap();
124+
125+
let mut buffer = Vec::new();
126+
let tree_iter = odb
127+
.find(&commit.tree(), &mut buffer)
128+
.unwrap()
129+
.try_into_tree_iter()
130+
.unwrap();
131+
132+
let mut recorder = gix::diff::tree::Recorder::default();
133+
gix::diff::tree(
134+
parent_tree_iter,
135+
tree_iter,
136+
gix::diff::tree::State::default(),
137+
&odb,
138+
&mut recorder,
139+
)
140+
.unwrap();
141+
142+
recorder
143+
.records
144+
.iter()
145+
.filter(|change| match change {
146+
gix::diff::tree::recorder::Change::Modification { path, .. } => path == file_path,
147+
gix::diff::tree::recorder::Change::Addition { path, .. } => path == file_path,
148+
_ => false,
149+
})
150+
.cloned()
151+
.collect()
152+
}

gitoxide-core/src/repository/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pub mod commitgraph;
4646
mod fsck;
4747
pub use fsck::function as fsck;
4848
pub mod index;
49+
pub mod log;
4950
pub mod mailmap;
5051
mod merge_base;
5152
pub use merge_base::merge_base;

src/plumbing/main.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,15 @@ pub fn main() -> Result<()> {
209209
},
210210
),
211211
},
212+
Subcommands::Log(crate::plumbing::options::log::Platform { pathspec }) => prepare_and_run(
213+
"log",
214+
trace,
215+
verbose,
216+
progress,
217+
progress_keep_open,
218+
None,
219+
move |_progress, out, _err| core::repository::log::log(repository(Mode::Lenient)?, out, pathspec),
220+
),
212221
Subcommands::Worktree(crate::plumbing::options::worktree::Platform { cmd }) => match cmd {
213222
crate::plumbing::options::worktree::SubCommands::List => prepare_and_run(
214223
"worktree-list",

src/plumbing/options/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ pub enum Subcommands {
146146
MergeBase(merge_base::Command),
147147
Merge(merge::Platform),
148148
Diff(diff::Platform),
149+
Log(log::Platform),
149150
Worktree(worktree::Platform),
150151
/// Subcommands that need no git repository to run.
151152
#[clap(subcommand)]
@@ -409,6 +410,18 @@ pub mod diff {
409410
}
410411
}
411412

413+
pub mod log {
414+
use gix::bstr::BString;
415+
416+
/// Print the history of a given file
417+
#[derive(Debug, clap::Parser)]
418+
pub struct Platform {
419+
/// The git path specification to show a log for.
420+
#[clap(value_parser = crate::shared::AsBString)]
421+
pub pathspec: BString,
422+
}
423+
}
424+
412425
pub mod config {
413426
use gix::bstr::BString;
414427

0 commit comments

Comments
 (0)