Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A couple examples #932

Merged
merged 10 commits into from
Jul 17, 2023
195 changes: 195 additions & 0 deletions examples/log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/// A toy-version of `git log`.
use clap::Parser;
use gix::bstr::{BString, ByteSlice};
use gix::{date::time::format, traverse::commit::Sorting};
use std::io::{stdout, Write};
use std::path::{Path, PathBuf};

fn main() {
let args = Args::parse_from(gix::env::args_os());
match run(args) {
Ok(()) => {}
Err(e) => eprintln!("error: {e}"),
}
}

#[derive(Debug, clap::Parser)]
#[clap(name = "log", about = "git log example", version = option_env!("GITOXIDE_VERSION"))]
struct Args {
/// Alternative git directory to use
#[clap(name = "dir", long = "git-dir")]
git_dir: Option<PathBuf>,
/// Number of commits to return
#[clap(short, long)]
count: Option<usize>,
/// Number of commits to skip
#[clap(short, long)]
skip: Option<usize>,
/// Commits are sorted as they are mentioned in the commit graph.
#[clap(short, long)]
breadth_first: bool,
/// Commits are sorted by their commit time in descending order.
#[clap(short, long)]
newest_first: bool,
/// Show commits with the specified minimum number of parents
#[clap(long)]
min_parents: Option<usize>,
/// Show commits with the specified maximum number of parents
#[clap(long)]
max_parents: Option<usize>,
/// Show only merge commits (implies --min-parents=2)
#[clap(long)]
merges: bool,
/// Show only non-merge commits (implies --max-parents=1)
#[clap(long)]
no_merges: bool,
/// Reverse the commit sort order (and loads all of them into memory).
#[clap(short, long)]
reverse: bool,
/// The ref-spec for the first commit to log, or HEAD.
#[clap(name = "commit")]
committish: Option<String>,
/// The path interested in log history of
#[clap(name = "path")]
paths: Vec<PathBuf>,
}

fn run(args: Args) -> anyhow::Result<()> {
let repo = gix::discover(args.git_dir.as_deref().unwrap_or(Path::new(".")))?;
let commit = repo
.rev_parse_single({
args.committish
.map(|mut c| {
c.push_str("^{commit}");
c
})
.as_deref()
.unwrap_or("HEAD")
})?
.object()?
.try_into_commit()?;

let sorting = if args.breadth_first {
Sorting::BreadthFirst
} else {
// else if args.newest_first {
Sorting::ByCommitTimeNewestFirst
};

let mut min_parents = args.min_parents.unwrap_or(0);
let mut max_parents = args.max_parents.unwrap_or(usize::MAX);
if args.merges {
min_parents = 2;
}
if args.no_merges {
max_parents = 1;
}

let mut log_iter: Box<dyn Iterator<Item = Result<LogEntryInfo, _>>> = Box::new(
repo.rev_walk([commit.id])
.sorting(sorting)
.all()?
.filter(|info| {
info.as_ref().map_or(true, |info| {
info.parent_ids.len() <= max_parents &&
info.parent_ids.len() >= min_parents &&
// if the list of paths is empty the filter passes.
// if paths are provided check that any one of them are
// in fact relevant for the current commit.
(args.paths.is_empty() || args.paths.iter().any(|path| {
// TODO: should make use of the `git2::DiffOptions`
// counterpart in gix for a set of files and also to
// generate diffs. When ready, also make paths resistant
// to illformed UTF8 by not using ".display()".
// PERFORMANCE WARNING: What follows is a clever implementation
// that is also **very** slow - do not use on bigger sample
// repositories as this needs native support in `gix` to
// be fast enough.
match repo.rev_parse_single(
format!("{}:{}", info.id, path.display()).as_str()
) {
// check by parsing the revspec on the path with
// the prefix of the tree of the current commit,
// vs. the same counterpart but using each of
// commit's parents; if any pairs don't match,
// this indicates this path was changed in this
// commit thus should be included in output.
// naturally, root commits have no parents and
// by definition whatever paths in there must
// have been introduced there, so include them.
Ok(oid) => info.parent_ids.is_empty() || info
.parent_ids
.iter()
.any(|id| {
repo.rev_parse_single(
format!("{id}:{}", path.display()).as_str()
).ok() != Some(oid)
}),
// no oid for the path resolved with this commit
// so this commit can be omitted from output.
Err(_) => false,
}
}))
})
})
.map(|info| -> anyhow::Result<_> {
let info = info?;
let commit = info.object()?;
let commit_ref = commit.decode()?;
Ok(LogEntryInfo {
commit_id: commit.id().to_hex().to_string(),
parents: info.parent_ids().map(|id| id.shorten_or_id().to_string()).collect(),
author: {
let mut buf = Vec::new();
commit_ref.author.write_to(&mut buf)?;
buf.into()
},
time: commit_ref.author.time.format(format::DEFAULT),
message: commit_ref.message.to_owned(),
})
}),
);
if args.reverse {
let mut results: Vec<_> = log_iter.collect();
results.reverse();
log_iter = Box::new(results.into_iter())
}

let mut log_iter = log_iter
.skip(args.skip.unwrap_or_default())
.take(args.count.unwrap_or(usize::MAX))
.peekable();

let mut out = stdout().lock();
let mut buf = Vec::new();
while let Some(entry) = log_iter.next() {
buf.clear();
let entry = entry?;
writeln!(buf, "commit {}", entry.commit_id)?;
if entry.parents.len() > 1 {
writeln!(buf, "Merge: {}", entry.parents.join(" "))?;
}
writeln!(buf, "Author: {}", entry.author)?;
writeln!(buf, "Date: {}\n", entry.time)?;
for line in entry.message.lines() {
write!(buf, " ")?;
buf.write_all(line)?;
writeln!(buf)?;
}
// only include newline if more log entries, mimicking `git log`
if log_iter.peek().is_some() {
writeln!(buf)?;
}
out.write_all(&buf)?;
}

Ok(())
}

struct LogEntryInfo {
commit_id: String,
parents: Vec<String>,
author: BString,
time: String,
message: BString,
}
85 changes: 85 additions & 0 deletions examples/ls-tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use clap::Parser;
use gix::bstr::BString;
use std::io::{stdout, Write};

use gix::{objs::tree::EntryMode, objs::tree::EntryMode::Tree, traverse::tree::Recorder, ObjectId};

fn main() {
let args = Args::parse_from(gix::env::args_os());
match run(args) {
Ok(()) => {}
Err(e) => eprintln!("error: {e}"),
}
}

#[derive(Debug, clap::Parser)]
#[clap(name = "ls-tree", about = "git ls-tree example", version = option_env!("GITOXIDE_VERSION"))]
#[clap(arg_required_else_help = true)]
struct Args {
/// Recurse into subtrees
#[clap(short = 'r')]
recursive: bool,
/// Only show trees
#[clap(short = 'd')]
tree_only: bool,
/// Show trees when recursing
#[clap(short = 't')]
tree_recursing: bool,
/// A revspec pointing to a tree-ish object, e.g. 'HEAD', 'HEAD:src/'
#[clap(name = "tree-ish")]
treeish: String,
}

fn run(mut args: Args) -> anyhow::Result<()> {
let repo = gix::discover(".")?;
let tree = repo
.rev_parse_single({
args.treeish.push_str("^{tree}");
&*args.treeish
})?
.object()?
.into_tree();
let entries = if args.recursive {
let mut recorder = Recorder::default();
tree.traverse().breadthfirst(&mut recorder)?;
recorder
.records
.into_iter()
.filter(|entry| args.tree_recursing || args.tree_only || entry.mode != Tree)
.filter(|entry| !args.tree_only || (entry.mode == Tree))
.map(|entry| Entry::new(entry.mode, entry.oid, entry.filepath))
.collect::<Vec<_>>()
} else {
tree.iter()
.filter_map(|res| res.ok().map(|entry| entry.inner)) // dropping errors silently
.filter(|entry| !args.tree_only || (entry.mode == Tree))
.map(|entry| Entry::new(entry.mode, entry.oid.to_owned(), entry.filename.to_owned()))
.collect::<Vec<_>>()
};

let mut out = stdout().lock();
for entry in entries {
writeln!(
out,
"{:06o} {:4} {} {}",
entry.kind as u16,
entry.kind.as_str(),
entry.hash,
entry.path
)?;
}

Ok(())
}

struct Entry {
kind: EntryMode,
hash: ObjectId,
path: BString,
}

impl Entry {
fn new(kind: EntryMode, hash: ObjectId, path: BString) -> Self {
Self { kind, hash, path }
}
}