Skip to content

Commit cdd3523

Browse files
cruesslerByron
authored andcommitted
feat: add first 'debug' version of gix log
It's primarily meant to better understand `gix blame`.
1 parent cbdbb8a commit cdd3523

File tree

4 files changed

+193
-0
lines changed

4 files changed

+193
-0
lines changed

gitoxide-core/src/repository/log.rs

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

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
@@ -269,6 +269,15 @@ pub fn main() -> Result<()> {
269269
},
270270
),
271271
},
272+
Subcommands::Log(crate::plumbing::options::log::Platform { pathspec }) => prepare_and_run(
273+
"log",
274+
trace,
275+
verbose,
276+
progress,
277+
progress_keep_open,
278+
None,
279+
move |_progress, out, _err| core::repository::log::log(repository(Mode::Lenient)?, out, pathspec),
280+
),
272281
Subcommands::Worktree(crate::plumbing::options::worktree::Platform { cmd }) => match cmd {
273282
crate::plumbing::options::worktree::SubCommands::List => prepare_and_run(
274283
"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)]
@@ -499,6 +500,18 @@ pub mod diff {
499500
}
500501
}
501502

503+
pub mod log {
504+
use gix::bstr::BString;
505+
506+
/// List all commits in a repository, optionally limited to those that change a given path
507+
#[derive(Debug, clap::Parser)]
508+
pub struct Platform {
509+
/// The git path specification to show a log for.
510+
#[clap(value_parser = crate::shared::AsBString)]
511+
pub pathspec: Option<BString>,
512+
}
513+
}
514+
502515
pub mod config {
503516
use gix::bstr::BString;
504517

0 commit comments

Comments
 (0)