use std::collections::{BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque};
use std::io::Read;
use std::path::Path;
use crate::error::{Error, Result};
use crate::objects::{parse_commit, parse_tag, parse_tree, ObjectId, ObjectKind};
use crate::refs;
use crate::repo::Repository;
use crate::rev_list::{
collect_revision_specs_with_stdin, date_order_walk, resolve_object_walk_roots,
resolve_revision_commits, walk_closure, walk_closure_ordered, CommitGraph, ObjectWalkRoot,
};
use crate::sparse_checkout::{path_in_sparse_checkout_patterns, ConePatterns};
const ROOT_PATH: &str = "";
const TAG_PATH: &str = "/tags";
const TAGGED_BLOBS_PATH: &str = "/tagged-blobs";
#[derive(Debug, Clone)]
pub struct PathWalkOptions {
pub include_commits: bool,
pub include_trees: bool,
pub include_blobs: bool,
pub include_tags: bool,
pub prune_all_uninteresting: bool,
pub edge_aggressive: bool,
pub cone_patterns: Option<ConePatterns>,
pub sparse_pattern_lines: Option<Vec<String>>,
}
impl Default for PathWalkOptions {
fn default() -> Self {
Self {
include_commits: true,
include_trees: true,
include_blobs: true,
include_tags: true,
prune_all_uninteresting: false,
edge_aggressive: false,
cone_patterns: None,
sparse_pattern_lines: None,
}
}
}
#[derive(Debug, Clone)]
pub struct PathWalkLine {
pub batch: u64,
pub object_kind: ObjectKind,
pub path: String,
pub oid: ObjectId,
pub uninteresting: bool,
}
#[derive(Debug, Clone, Default)]
pub struct PathWalkCounts {
pub commits: u64,
pub trees: u64,
pub blobs: u64,
pub tags: u64,
}
pub fn walk_objects_by_path(
repo: &Repository,
positive_specs: &[String],
negative_specs: &[String],
stdin_all: bool,
boundary: bool,
opts: &PathWalkOptions,
) -> Result<(Vec<PathWalkLine>, PathWalkCounts)> {
let mut graph = CommitGraph::new(repo, false);
let mut all_refs = stdin_all;
let mut filtered: Vec<String> = Vec::new();
let mut want_indexed_objects = false;
for p in positive_specs {
match p.as_str() {
"--all" => all_refs = true,
"--indexed-objects" => want_indexed_objects = true,
"--branches" => filtered.extend(expand_branches_refs(repo)?),
_ => filtered.push(p.clone()),
}
}
let mut neg_resolved: Vec<String> = Vec::new();
for n in negative_specs {
match n.as_str() {
"--all" => {
return Err(Error::InvalidRef(
"--all is not valid in negative revision list".to_owned(),
));
}
"--indexed-objects" => {
return Err(Error::InvalidRef(
"--indexed-objects is not valid in negative revision list".to_owned(),
));
}
"--branches" => neg_resolved.extend(expand_branches_refs(repo)?),
_ => neg_resolved.push(n.clone()),
}
}
if all_refs {
filtered.extend(all_ref_commits(repo)?);
}
if filtered.is_empty() && !want_indexed_objects && !all_refs {
return Err(Error::InvalidRef("no revisions specified".to_owned()));
}
let (commit_tips, mut object_roots) = if filtered.is_empty() {
(Vec::new(), Vec::new())
} else {
resolve_object_walk_roots(repo, &filtered)?
};
object_roots.extend(tag_object_roots_from_spec_names(repo, &filtered)?);
if want_indexed_objects {
object_roots.extend(indexed_blob_roots(repo)?);
}
let extra_tag_ref_targets: Vec<ObjectId> = if all_refs {
tag_ref_direct_targets(repo)?
} else {
Vec::new()
};
let exclude = resolve_revision_commits(repo, &neg_resolved)?;
let exclude_tips: HashSet<ObjectId> = exclude.iter().copied().collect();
let excluded: HashSet<ObjectId> = if exclude.is_empty() {
HashSet::new()
} else {
walk_closure(&mut graph, &exclude)?
};
let (included_commits, _) = if commit_tips.is_empty() {
(HashSet::new(), Vec::new())
} else {
walk_closure_ordered(&mut graph, &commit_tips)?
};
let mut interesting_commits: HashSet<ObjectId> = included_commits
.iter()
.copied()
.filter(|c| !excluded.contains(c))
.collect();
let boundary_commits: HashSet<ObjectId> = if boundary {
let inc: HashSet<ObjectId> = interesting_commits.iter().copied().collect();
let mut bset = HashSet::new();
for &oid in &interesting_commits {
for p in graph.parents_of(oid)? {
if !inc.contains(&p) && excluded.contains(&p) {
bset.insert(p);
}
}
}
interesting_commits.extend(bset.iter().copied());
bset
} else {
HashSet::new()
};
let mut uninteresting_commits: HashSet<ObjectId> = excluded.iter().copied().collect();
uninteresting_commits.retain(|c| interesting_commits.contains(c));
let excluded_objects: HashSet<ObjectId> = tree_blob_closure_from_commits(repo, &excluded)?;
let excluded_blob_paths = excluded_blob_paths_from_commits(repo, &excluded)?;
let mut ctx = PathWalkContext {
repo,
opts,
paths: HashMap::new(),
heap: BinaryHeap::new(),
pushed: HashSet::new(),
seen_object: HashSet::new(),
uninteresting_object: HashSet::new(),
excluded_objects,
excluded_blob_paths,
heap_seq: 0,
batch: 0,
lines: Vec::new(),
counts: PathWalkCounts::default(),
};
mark_uninteresting_trees(
repo,
&mut graph,
&interesting_commits,
&excluded,
&exclude_tips,
opts,
&mut ctx,
)?;
if opts.include_trees {
ctx.ensure_root_list();
ctx.push_heap(ROOT_PATH);
}
setup_pending_objects(repo, &object_roots, &extra_tag_ref_targets, opts, &mut ctx)?;
let ordered_commits = date_order_walk(&mut graph, &interesting_commits, false)?;
let mut commit_oids: Vec<ObjectId> = Vec::new();
for c in ordered_commits {
if !interesting_commits.contains(&c) {
continue;
}
if opts.include_commits {
commit_oids.push(c);
}
if !opts.include_trees && !opts.include_blobs {
continue;
}
let commit = load_commit_data(repo, c)?;
let tree_oid = commit.tree;
if ctx.seen_object.contains(&tree_oid) {
continue;
}
ctx.seen_object.insert(tree_oid);
if ctx.excluded_objects.contains(&tree_oid) {
ctx.uninteresting_object.insert(tree_oid);
}
ctx.append_root_tree(tree_oid)?;
}
if opts.edge_aggressive && opts.include_trees {
for &tip in &exclude_tips {
let commit = load_commit_data(repo, tip)?;
let tree_oid = commit.tree;
if ctx.seen_object.contains(&tree_oid) {
continue;
}
ctx.seen_object.insert(tree_oid);
ctx.uninteresting_object.insert(tree_oid);
ctx.append_root_tree(tree_oid)?;
}
}
if opts.include_commits && !commit_oids.is_empty() {
ctx.emit_commit_batch(&commit_oids, &uninteresting_commits, &boundary_commits)?;
}
ctx.drain_heap(&mut graph)?;
if !ctx.paths.is_empty() {
for key in ctx.paths.keys().cloned().collect::<Vec<_>>() {
ctx.push_heap(&key);
}
ctx.drain_heap(&mut graph)?;
}
Ok((ctx.lines, ctx.counts))
}
fn tag_object_roots_from_spec_names(
repo: &Repository,
specs: &[String],
) -> Result<Vec<ObjectWalkRoot>> {
let mut out = Vec::new();
let mut seen = HashSet::new();
for spec in specs {
if spec.contains("..") || spec.starts_with('^') || spec == "HEAD" {
continue;
}
if spec.len() == 40 && spec.chars().all(|c| c.is_ascii_hexdigit()) {
continue;
}
let refname = if spec.starts_with("refs/") {
spec.clone()
} else {
format!("refs/tags/{spec}")
};
let Ok(oid) = refs::resolve_ref(&repo.git_dir, &refname) else {
continue;
};
let obj = repo.odb.read(&oid)?;
if obj.kind != ObjectKind::Tag {
continue;
}
if seen.insert(oid) {
out.push(ObjectWalkRoot {
oid,
input: spec.clone(),
root_path: None,
});
}
}
Ok(out)
}
fn tag_ref_direct_targets(repo: &Repository) -> Result<Vec<ObjectId>> {
let mut out = Vec::new();
for (name, _) in refs::list_refs(&repo.git_dir, "refs/tags/")? {
let oid = refs::resolve_ref(&repo.git_dir, &name)?;
out.push(oid);
}
Ok(out)
}
fn all_ref_commits(repo: &Repository) -> Result<Vec<String>> {
let mut specs = Vec::new();
specs.push("HEAD".to_owned());
for (name, _) in refs::list_refs(&repo.git_dir, "refs/")? {
specs.push(name);
}
Ok(specs)
}
pub fn expand_branches_refs(repo: &Repository) -> Result<Vec<String>> {
let mut out = Vec::new();
for (_, oid) in refs::list_refs(&repo.git_dir, "refs/heads/")? {
out.push(oid.to_hex());
}
Ok(out)
}
fn indexed_blob_roots(repo: &Repository) -> Result<Vec<ObjectWalkRoot>> {
let Some(_) = &repo.work_tree else {
return Ok(Vec::new());
};
let index_path = repo.git_dir.join("index");
if !index_path.is_file() {
return Ok(Vec::new());
}
let idx = crate::index::Index::load(&index_path)?;
let mut out = Vec::new();
if let Some(root) = idx.cache_tree_root {
out.push(ObjectWalkRoot {
oid: root,
input: String::new(),
root_path: None,
});
}
let head_tree = head_tree_oid(repo).ok();
let mut file_entries: Vec<(String, ObjectId)> = Vec::new();
for e in &idx.entries {
if e.stage() != 0 {
continue;
}
if e.mode == 0o160000 || e.mode == crate::index::MODE_TREE {
continue;
}
let path_str = String::from_utf8_lossy(&e.path).into_owned();
file_entries.push((path_str.clone(), e.oid));
out.push(ObjectWalkRoot {
oid: e.oid,
input: format!(":{path_str}"),
root_path: Some(path_str),
});
}
let mut prefixes: BTreeSet<String> = BTreeSet::new();
for (path, _) in &file_entries {
let mut end = path.len();
while end > 0 {
if let Some(pos) = path[..end].rfind('/') {
prefixes.insert(path[..pos].to_string());
end = pos;
} else {
break;
}
}
}
let mut recovery_added: HashSet<String> = HashSet::new();
let Some(ht) = head_tree else {
return Ok(out);
};
for pref in prefixes {
if pref.is_empty() {
continue;
}
let mut any_under = false;
let mut all_match = true;
for (path, oid) in &file_entries {
let under = if path == &pref {
true
} else if let Some(rest) = path.strip_prefix(&pref) {
rest.starts_with('/')
} else {
false
};
if !under {
continue;
}
any_under = true;
match crate::rev_parse::resolve_treeish_path(repo, ht, path.as_str()) {
Ok(h) if h == *oid => {}
_ => {
all_match = false;
break;
}
}
}
if !any_under || !all_match {
continue;
}
let tree_oid = crate::rev_parse::resolve_treeish_path(repo, ht, pref.as_str())?;
if !recovery_added.insert(pref.clone()) {
continue;
}
out.push(ObjectWalkRoot {
oid: tree_oid,
input: String::new(),
root_path: Some(pref),
});
}
Ok(out)
}
fn head_tree_oid(repo: &Repository) -> Result<ObjectId> {
let head = refs::resolve_ref(&repo.git_dir, "HEAD")?;
let obj = repo.odb.read(&head)?;
if obj.kind != ObjectKind::Commit {
return Err(Error::InvalidRef("HEAD is not a commit".to_owned()));
}
let c = parse_commit(&obj.data)?;
Ok(c.tree)
}
struct TypeOidList {
kind: ObjectKind,
oids: Vec<ObjectId>,
maybe_interesting: bool,
}
struct PathWalkContext<'a> {
repo: &'a Repository,
opts: &'a PathWalkOptions,
paths: HashMap<String, TypeOidList>,
heap: BinaryHeap<std::cmp::Reverse<PathHeapItem>>,
pushed: HashSet<String>,
seen_object: HashSet<ObjectId>,
uninteresting_object: HashSet<ObjectId>,
excluded_objects: HashSet<ObjectId>,
excluded_blob_paths: HashSet<(String, ObjectId)>,
heap_seq: u64,
batch: u64,
lines: Vec<PathWalkLine>,
counts: PathWalkCounts,
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
struct PathHeapItem {
type_rank: u8,
path: String,
seq: u64,
}
fn type_rank(kind: ObjectKind) -> u8 {
match kind {
ObjectKind::Tag => 0,
ObjectKind::Blob => 1,
ObjectKind::Tree => 2,
ObjectKind::Commit => 3,
}
}
impl<'a> PathWalkContext<'a> {
fn ensure_root_list(&mut self) {
self.paths
.entry(ROOT_PATH.to_string())
.or_insert_with(|| TypeOidList {
kind: ObjectKind::Tree,
oids: Vec::new(),
maybe_interesting: true,
});
}
fn push_heap(&mut self, path: &str) {
if !self.pushed.insert(path.to_string()) {
return;
}
let Some(list) = self.paths.get(path) else {
return;
};
let seq = self.heap_seq;
self.heap_seq = self.heap_seq.wrapping_add(1);
self.heap.push(std::cmp::Reverse(PathHeapItem {
type_rank: type_rank(list.kind),
path: path.to_string(),
seq,
}));
}
fn append_root_tree(&mut self, oid: ObjectId) -> Result<()> {
self.ensure_root_list();
let list = self.paths.get_mut(ROOT_PATH).expect("root list");
list.oids.push(oid);
self.push_heap(ROOT_PATH);
Ok(())
}
fn add_path(
&mut self,
path: String,
kind: ObjectKind,
oid: ObjectId,
interesting: bool,
) -> Result<()> {
self.add_path_inner(path, kind, oid, interesting, true)
}
fn add_path_pending(
&mut self,
path: String,
kind: ObjectKind,
oid: ObjectId,
interesting: bool,
) -> Result<()> {
self.add_path_inner(path, kind, oid, interesting, false)
}
fn add_path_inner(
&mut self,
path: String,
kind: ObjectKind,
oid: ObjectId,
interesting: bool,
enqueue: bool,
) -> Result<()> {
let list = self
.paths
.entry(path.clone())
.or_insert_with(|| TypeOidList {
kind,
oids: Vec::new(),
maybe_interesting: false,
});
if list.kind != kind {
return Err(Error::CorruptObject(format!(
"path-walk: inconsistent types at path {path:?}"
)));
}
list.maybe_interesting |= interesting;
list.oids.push(oid);
if enqueue {
self.push_heap(&path);
}
Ok(())
}
fn cone_allows_tree_path(&self, path_with_slash: &str) -> bool {
if let Some(lines) = &self.opts.sparse_pattern_lines {
return path_in_sparse_checkout_patterns(path_with_slash, lines, true);
}
if let Some(cone) = &self.opts.cone_patterns {
let trimmed = path_with_slash.trim_end_matches('/');
if cone.path_included(trimmed) {
return true;
}
return cone.path_included(path_with_slash);
}
true
}
fn cone_allows_blob_path(&self, path: &str) -> bool {
if let Some(lines) = &self.opts.sparse_pattern_lines {
return path_in_sparse_checkout_patterns(path, lines, true);
}
if let Some(cone) = &self.opts.cone_patterns {
return cone.path_included(path);
}
true
}
fn emit_commit_batch(
&mut self,
oids: &[ObjectId],
uninteresting: &HashSet<ObjectId>,
boundary: &HashSet<ObjectId>,
) -> Result<()> {
let batch = self.batch;
self.batch += 1;
for &oid in oids {
let u = uninteresting.contains(&oid) || boundary.contains(&oid);
self.lines.push(PathWalkLine {
batch,
object_kind: ObjectKind::Commit,
path: ROOT_PATH.to_string(),
oid,
uninteresting: u,
});
self.counts.commits += 1;
}
Ok(())
}
fn drain_heap(&mut self, graph: &mut CommitGraph<'_>) -> Result<()> {
while let Some(std::cmp::Reverse(item)) = self.heap.pop() {
self.walk_path(&item.path, graph)?;
}
Ok(())
}
fn walk_path(&mut self, path: &str, graph: &mut CommitGraph<'_>) -> Result<()> {
let Some(mut list) = self.paths.remove(path) else {
return Ok(());
};
if list.oids.is_empty() {
return Ok(());
}
if self.opts.prune_all_uninteresting {
if !list.maybe_interesting {
return Ok(());
}
list.maybe_interesting = false;
for oid in &list.oids {
if self.object_is_interesting_at_path(path, *oid, list.kind) {
list.maybe_interesting = true;
break;
}
}
if !list.maybe_interesting {
return Ok(());
}
}
let emit = match list.kind {
ObjectKind::Tree => self.opts.include_trees,
ObjectKind::Blob => self.opts.include_blobs,
ObjectKind::Tag => self.opts.include_tags,
_ => false,
};
let batch = if emit {
let b = self.batch;
self.batch += 1;
b
} else {
0
};
match list.kind {
ObjectKind::Tree if self.opts.include_trees => {
for &oid in &list.oids {
let u = !self.tree_is_interesting(oid);
self.lines.push(PathWalkLine {
batch,
object_kind: ObjectKind::Tree,
path: path.to_string(),
oid,
uninteresting: u,
});
self.counts.trees += 1;
}
}
ObjectKind::Blob if self.opts.include_blobs => {
for &oid in &list.oids {
let u = !self.blob_is_interesting_at_path(path, oid);
self.lines.push(PathWalkLine {
batch,
object_kind: ObjectKind::Blob,
path: path.to_string(),
oid,
uninteresting: u,
});
self.counts.blobs += 1;
}
}
ObjectKind::Tag if self.opts.include_tags => {
for &oid in &list.oids {
self.lines.push(PathWalkLine {
batch,
object_kind: ObjectKind::Tag,
path: path.to_string(),
oid,
uninteresting: false,
});
self.counts.tags += 1;
}
}
_ => {}
}
if list.kind == ObjectKind::Tree {
let tree_oids = list.oids.clone();
for tree_oid in tree_oids {
self.add_tree_entries(path, tree_oid, graph)?;
}
}
Ok(())
}
fn add_tree_entries(
&mut self,
base_path: &str,
tree_oid: ObjectId,
_graph: &mut CommitGraph<'_>,
) -> Result<()> {
let obj = self.repo.odb.read(&tree_oid)?;
if obj.kind != ObjectKind::Tree {
return Err(Error::CorruptObject(format!("{tree_oid} is not a tree")));
}
let parent_uninteresting = !self.tree_is_interesting(tree_oid);
let entries = parse_tree(&obj.data)?;
for entry in entries {
if entry.mode == 0o160000 {
continue;
}
let is_tree = entry.mode == 0o040000;
if !self.opts.include_blobs && !is_tree {
continue;
}
let name = String::from_utf8_lossy(&entry.name);
let child_oid = entry.oid;
if self.seen_object.contains(&child_oid) {
continue;
}
self.seen_object.insert(child_oid);
if parent_uninteresting {
self.uninteresting_object.insert(child_oid);
}
if is_tree {
let rel = if base_path.is_empty() {
format!("{name}/")
} else {
format!("{base_path}{name}/")
};
if !self.cone_allows_tree_path(&rel) {
continue;
}
self.add_path(
rel,
ObjectKind::Tree,
child_oid,
self.tree_is_interesting(child_oid) || !parent_uninteresting,
)?;
} else {
let rel = if base_path.is_empty() {
name.into_owned()
} else {
format!("{base_path}{}", name.as_ref())
};
if !self.cone_allows_blob_path(&rel) {
continue;
}
let blob_interesting =
self.blob_is_interesting_at_path(&rel, child_oid) || !parent_uninteresting;
self.add_path(rel, ObjectKind::Blob, child_oid, blob_interesting)?;
}
}
Ok(())
}
fn object_is_interesting_at_path(&self, path: &str, oid: ObjectId, kind: ObjectKind) -> bool {
match kind {
ObjectKind::Blob => self.blob_is_interesting_at_path(path, oid),
ObjectKind::Tree => self.tree_is_interesting(oid),
_ => true,
}
}
fn tree_is_interesting(&self, oid: ObjectId) -> bool {
!self.uninteresting_object.contains(&oid) && !self.excluded_objects.contains(&oid)
}
fn blob_is_interesting_at_path(&self, path: &str, oid: ObjectId) -> bool {
!self.uninteresting_object.contains(&oid)
&& !self.excluded_blob_paths.contains(&(path.to_string(), oid))
}
}
fn excluded_blob_paths_from_commits(
repo: &Repository,
commits: &HashSet<ObjectId>,
) -> Result<HashSet<(String, ObjectId)>> {
let mut out = HashSet::new();
for &c in commits {
let commit = match load_commit_data(repo, c) {
Ok(co) => co,
Err(_) => continue,
};
collect_blob_paths(repo, commit.tree, "", &mut out)?;
}
Ok(out)
}
fn collect_blob_paths(
repo: &Repository,
tree_oid: ObjectId,
base: &str,
into: &mut HashSet<(String, ObjectId)>,
) -> Result<()> {
let obj = repo.odb.read(&tree_oid)?;
if obj.kind != ObjectKind::Tree {
return Ok(());
}
let entries = parse_tree(&obj.data)?;
for e in entries {
if e.mode == 0o160000 {
continue;
}
let name = String::from_utf8_lossy(&e.name);
if e.mode == 0o040000 {
let next_base = if base.is_empty() {
format!("{name}/")
} else {
format!("{base}{name}/")
};
collect_blob_paths(repo, e.oid, &next_base, into)?;
} else {
let path = if base.is_empty() {
name.into_owned()
} else {
format!("{base}{}", name.as_ref())
};
into.insert((path, e.oid));
}
}
Ok(())
}
fn tree_blob_closure_from_commits(
repo: &Repository,
commits: &HashSet<ObjectId>,
) -> Result<HashSet<ObjectId>> {
let mut out = HashSet::new();
for &c in commits {
let commit = match load_commit_data(repo, c) {
Ok(co) => co,
Err(_) => continue,
};
let mut stack = vec![commit.tree];
while let Some(t) = stack.pop() {
if !out.insert(t) {
continue;
}
let obj = repo.odb.read(&t)?;
if obj.kind != ObjectKind::Tree {
continue;
}
let entries = parse_tree(&obj.data)?;
for e in entries {
if e.mode == 0o160000 {
continue;
}
out.insert(e.oid);
if e.mode == 0o040000 {
stack.push(e.oid);
}
}
}
}
Ok(out)
}
fn load_commit_data(repo: &Repository, oid: ObjectId) -> Result<crate::objects::CommitData> {
let object = repo.odb.read(&oid)?;
if object.kind != ObjectKind::Commit {
return Err(Error::CorruptObject(format!("{oid} is not a commit")));
}
parse_commit(&object.data)
}
fn mark_uninteresting_trees(
repo: &Repository,
graph: &mut CommitGraph<'_>,
interesting: &HashSet<ObjectId>,
excluded: &HashSet<ObjectId>,
exclude_tips: &HashSet<ObjectId>,
opts: &PathWalkOptions,
ctx: &mut PathWalkContext<'_>,
) -> Result<()> {
if !opts.prune_all_uninteresting && !opts.edge_aggressive {
return Ok(());
}
let mut queue: VecDeque<ObjectId> = interesting.iter().copied().collect();
let mut seen_edge_commit = HashSet::new();
while let Some(c) = queue.pop_front() {
let parents = graph.parents_of(c)?;
for p in parents {
if interesting.contains(&p) {
continue;
}
if !excluded.contains(&p) {
continue;
}
if !seen_edge_commit.insert(p) {
continue;
}
let commit = load_commit_data(repo, p)?;
ctx.uninteresting_object.insert(commit.tree);
queue.push_back(p);
}
}
if opts.edge_aggressive {
for &c in exclude_tips {
if seen_edge_commit.contains(&c) {
continue;
}
let Ok(commit) = load_commit_data(repo, c) else {
continue;
};
ctx.uninteresting_object.insert(commit.tree);
}
}
Ok(())
}
fn setup_pending_objects(
repo: &Repository,
roots: &[ObjectWalkRoot],
extra_tag_refs: &[ObjectId],
opts: &PathWalkOptions,
ctx: &mut PathWalkContext<'_>,
) -> Result<()> {
let mut tag_oids: Vec<ObjectId> = Vec::new();
let mut tagged_blob_oids: Vec<ObjectId> = Vec::new();
let mut tag_seen: HashSet<ObjectId> = HashSet::new();
for &oid in extra_tag_refs {
if ctx.seen_object.contains(&oid) {
continue;
}
let obj = repo.odb.read(&oid)?;
match obj.kind {
ObjectKind::Tag | ObjectKind::Commit => {
if tag_seen.insert(oid) {
tag_oids.push(oid);
}
}
ObjectKind::Tree if opts.include_trees => {
ctx.seen_object.insert(oid);
ctx.append_root_tree(oid)?;
}
ObjectKind::Blob if opts.include_blobs => {
ctx.seen_object.insert(oid);
tagged_blob_oids.push(oid);
}
_ => {}
}
}
for root in roots {
let mut oid = root.oid;
let mut obj = repo.odb.read(&oid)?;
while obj.kind == ObjectKind::Tag {
if ctx.seen_object.contains(&oid) {
break;
}
ctx.seen_object.insert(oid);
if opts.include_tags && tag_seen.insert(oid) {
tag_oids.push(oid);
}
let tag = parse_tag(&obj.data)?;
oid = tag.object;
obj = repo.odb.read(&oid)?;
}
if obj.kind == ObjectKind::Tag {
continue;
}
if !ctx.seen_object.insert(oid) {
continue;
}
match obj.kind {
ObjectKind::Tree if opts.include_trees => {
if let Some(p) = &root.root_path {
let trimmed = p.trim_end_matches('/');
let path = if trimmed.is_empty() {
"/".to_string()
} else {
format!("{trimmed}/")
};
ctx.add_path_pending(path, ObjectKind::Tree, oid, true)?;
} else {
ctx.ensure_root_list();
ctx.paths.get_mut(ROOT_PATH).expect("root").oids.push(oid);
ctx.push_heap(ROOT_PATH);
}
}
ObjectKind::Blob if opts.include_blobs => {
if let Some(p) = &root.root_path {
ctx.add_path_pending(p.clone(), ObjectKind::Blob, oid, true)?;
} else {
tagged_blob_oids.push(oid);
}
}
ObjectKind::Commit => {}
_ => {}
}
}
if opts.include_blobs && !tagged_blob_oids.is_empty() {
let list = TypeOidList {
kind: ObjectKind::Blob,
oids: tagged_blob_oids,
maybe_interesting: true,
};
ctx.paths.insert(TAGGED_BLOBS_PATH.to_string(), list);
ctx.push_heap(TAGGED_BLOBS_PATH);
}
if opts.include_tags && !tag_oids.is_empty() {
let list = TypeOidList {
kind: ObjectKind::Tag,
oids: tag_oids,
maybe_interesting: true,
};
ctx.paths.insert(TAG_PATH.to_string(), list);
ctx.push_heap(TAG_PATH);
}
Ok(())
}
pub fn parse_path_walk_cli(
git_dir: &Path,
args: &[String],
) -> Result<(PathWalkOptions, Vec<String>, Vec<String>, bool, bool)> {
let mut opts = PathWalkOptions::default();
let mut positive = Vec::new();
let mut negative = Vec::new();
let mut stdin_all = false;
let mut boundary_flag = false;
let mut after_dd = false;
let mut not_mode = false;
let mut i = 0usize;
while i < args.len() {
let a = &args[i];
if !after_dd && a == "--" {
after_dd = true;
i += 1;
continue;
}
if !after_dd {
match a.as_str() {
"--stdin-pl" => {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
let lines: Vec<String> = buf
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.map(String::from)
.collect();
if !lines.is_empty() {
opts.sparse_pattern_lines = Some(lines);
}
}
"--prune" => opts.prune_all_uninteresting = true,
"--edge-aggressive" => opts.edge_aggressive = true,
"--no-blobs" => opts.include_blobs = false,
"--no-trees" => opts.include_trees = false,
"--no-commits" => opts.include_commits = false,
"--no-tags" => opts.include_tags = false,
"--blobs" => opts.include_blobs = true,
"--trees" => opts.include_trees = true,
"--commits" => opts.include_commits = true,
"--tags" => opts.include_tags = true,
"--stdin" => {
let (pos, neg, all, _stdin_paths) =
collect_revision_specs_with_stdin(git_dir, &[], true)?;
stdin_all |= all;
positive.extend(pos);
negative.extend(neg);
}
_ => {
return Err(Error::InvalidRef(format!(
"path-walk: unknown option '{a}'"
)));
}
}
} else if a == "--not" {
not_mode = !not_mode;
} else if a == "--boundary" {
boundary_flag = true;
} else if matches!(a.as_str(), "--all" | "--indexed-objects" | "--branches") {
if not_mode {
negative.push(a.clone());
} else {
positive.push(a.clone());
}
} else {
let (p, n) = crate::rev_list::split_revision_token(a);
if not_mode {
negative.extend(p);
positive.extend(n);
} else {
positive.extend(p);
negative.extend(n);
}
}
i += 1;
}
Ok((opts, positive, negative, stdin_all, boundary_flag))
}