1684 lines
56 KiB
Rust
1684 lines
56 KiB
Rust
//! Local Git operations adapted from the v2 gix plugin, without RPC or host imports.
|
|
#![allow(dead_code)]
|
|
mod archive;
|
|
mod branches;
|
|
mod changes;
|
|
mod checkout;
|
|
mod http;
|
|
mod ssh;
|
|
mod merge;
|
|
mod network;
|
|
mod output;
|
|
mod push;
|
|
mod rebase;
|
|
mod remotes;
|
|
mod repo;
|
|
mod staging;
|
|
mod worktrees;
|
|
use gix::bstr::ByteSlice;
|
|
use repo::error;
|
|
#[derive(Debug)]
|
|
pub struct Error {
|
|
pub message: String,
|
|
}
|
|
impl Error {
|
|
pub fn new(_: i32, message: String) -> Self {
|
|
Self { message }
|
|
}
|
|
}
|
|
impl std::fmt::Display for Error {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str(&self.message)
|
|
}
|
|
}
|
|
impl std::error::Error for Error {}
|
|
pub type Result<T> = std::result::Result<T, Error>;
|
|
#[derive(serde::Serialize, serde::Deserialize)]
|
|
pub struct HeadInfo {
|
|
detached: bool,
|
|
unborn: bool,
|
|
branch: Option<String>,
|
|
id: Option<String>,
|
|
}
|
|
include!("head.inc");
|
|
struct Identity {
|
|
name: Option<String>,
|
|
email: Option<String>,
|
|
}
|
|
fn identity(name: Option<String>, email: Option<String>) -> Result<Identity> {
|
|
let repo = gix::discover(".").map_err(|e| error("identity", e))?;
|
|
let config = repo.config_snapshot();
|
|
Ok(Identity {
|
|
name: name
|
|
.or_else(|| std::env::var("GIT_AUTHOR_NAME").ok())
|
|
.or_else(|| config.string("user.name").map(|s| s.to_string())),
|
|
email: email
|
|
.or_else(|| std::env::var("GIT_AUTHOR_EMAIL").ok())
|
|
.or_else(|| config.string("user.email").map(|s| s.to_string())),
|
|
})
|
|
}
|
|
mod cherry_pick;
|
|
mod history;
|
|
mod local;
|
|
mod inspect;
|
|
mod plumbing;
|
|
mod pretty;
|
|
mod stash;
|
|
use clap::{Parser, Subcommand};
|
|
#[derive(Parser)]
|
|
#[command(name = "git", disable_help_subcommand = true)]
|
|
pub struct Cli {
|
|
#[arg(short = 'C', global = true)]
|
|
directory: Option<std::path::PathBuf>,
|
|
#[arg(short = 'c', global = true)]
|
|
config: Vec<String>,
|
|
#[arg(short = 'q', long, global = true)]
|
|
quiet: bool,
|
|
#[command(subcommand)]
|
|
command: Command,
|
|
}
|
|
#[derive(Subcommand)]
|
|
enum Command {
|
|
Archive {
|
|
#[arg(long, default_value = "tar", value_parser = ["tar"])]
|
|
format: String,
|
|
#[arg(short = 'o', long)]
|
|
output: Option<std::path::PathBuf>,
|
|
revision: String,
|
|
path: Option<String>,
|
|
},
|
|
Show {
|
|
#[arg(short = 's', long = "no-patch")]
|
|
no_patch: bool,
|
|
#[arg(long)]
|
|
format: Option<String>,
|
|
#[arg(long)]
|
|
pretty: Option<String>,
|
|
#[arg(long)]
|
|
stat: bool,
|
|
#[arg(long)]
|
|
oneline: bool,
|
|
#[arg(long)]
|
|
name_only: bool,
|
|
revision: Option<String>,
|
|
},
|
|
#[command(group(clap::ArgGroup::new("mode").required(true).multiple(false)))]
|
|
CatFile {
|
|
#[arg(short = 't', group = "mode")]
|
|
kind: bool,
|
|
#[arg(short = 'p', group = "mode")]
|
|
pretty: bool,
|
|
#[arg(short = 's', group = "mode")]
|
|
size: bool,
|
|
#[arg(short = 'e', group = "mode")]
|
|
exists: bool,
|
|
revision: String,
|
|
},
|
|
LsTree {
|
|
#[arg(short = 'r')]
|
|
recursive: bool,
|
|
#[arg(long)]
|
|
name_only: bool,
|
|
revision: String,
|
|
path: Option<String>,
|
|
},
|
|
Clone {
|
|
url: String,
|
|
path: Option<String>,
|
|
},
|
|
Fetch {
|
|
#[arg(default_value = "origin")]
|
|
remote: String,
|
|
revision: Option<String>,
|
|
},
|
|
MergeBase {
|
|
#[arg(long)]
|
|
is_ancestor: bool,
|
|
a: String,
|
|
b: String,
|
|
},
|
|
RevList {
|
|
#[arg(long)]
|
|
count: bool,
|
|
revisions: Vec<String>,
|
|
},
|
|
SymbolicRef {
|
|
#[arg(long)]
|
|
short: bool,
|
|
name: String,
|
|
},
|
|
UpdateRef {
|
|
name: String,
|
|
new: String,
|
|
old: Option<String>,
|
|
},
|
|
Clean {
|
|
#[arg(short = 'f')]
|
|
force: bool,
|
|
#[arg(short = 'd')]
|
|
directories: bool,
|
|
},
|
|
CheckIgnore {
|
|
#[arg(long)]
|
|
no_index: bool,
|
|
paths: Vec<String>,
|
|
},
|
|
Push {
|
|
#[arg(short = 'u', long)]
|
|
set_upstream: bool,
|
|
#[arg(short = 'f', long)]
|
|
force: bool,
|
|
#[arg(default_value = "origin")]
|
|
remote: String,
|
|
refspec: Option<String>,
|
|
},
|
|
Init {
|
|
#[arg(long)]
|
|
bare: bool,
|
|
#[arg(short = 'b', default_value = "main")]
|
|
initial_branch: String,
|
|
path: Option<String>,
|
|
},
|
|
Config {
|
|
#[arg(long)]
|
|
local: bool,
|
|
#[arg(long)]
|
|
get: bool,
|
|
key: String,
|
|
value: Option<String>,
|
|
},
|
|
RevParse {
|
|
#[arg(long)]
|
|
verify: bool,
|
|
#[arg(long)]
|
|
show_toplevel: bool,
|
|
#[arg(long)]
|
|
git_dir: bool,
|
|
#[arg(long)]
|
|
abbrev_ref: bool,
|
|
#[arg(long)]
|
|
short: bool,
|
|
revisions: Vec<String>,
|
|
},
|
|
Status {
|
|
#[arg(long, default_value = "normal")]
|
|
untracked_files: String,
|
|
#[arg(short = 'z')]
|
|
nul: bool,
|
|
#[arg(short, long)]
|
|
short: bool,
|
|
#[arg(long, num_args = 0..=1, default_missing_value = "v1", require_equals = true)]
|
|
porcelain: Option<String>,
|
|
#[arg(short = 'b', long)]
|
|
branch: bool,
|
|
},
|
|
Add {
|
|
#[arg(short = 'A', long)]
|
|
all: bool,
|
|
paths: Vec<String>,
|
|
},
|
|
Rm {
|
|
#[arg(long)]
|
|
cached: bool,
|
|
paths: Vec<String>,
|
|
},
|
|
Commit {
|
|
#[arg(short = 'm')]
|
|
message: Option<String>,
|
|
#[arg(short = 'F', conflicts_with = "message")]
|
|
file: Option<String>,
|
|
#[arg(long)]
|
|
amend: bool,
|
|
#[arg(long)]
|
|
no_edit: bool,
|
|
#[arg(short, long)]
|
|
all: bool,
|
|
#[arg(long)]
|
|
allow_empty: bool,
|
|
/// Accepted for compatibility. This git never signs commits.
|
|
#[arg(long)]
|
|
no_gpg_sign: bool,
|
|
},
|
|
Log {
|
|
#[arg(long, num_args = 0..=1, default_missing_value = "short", require_equals = true, value_parser = ["short"])]
|
|
decorate: Option<String>,
|
|
#[arg(long)]
|
|
name_only: bool,
|
|
#[arg(long)]
|
|
no_renames: bool,
|
|
#[arg(long)]
|
|
diff_merges: Option<String>,
|
|
#[arg(long)]
|
|
oneline: bool,
|
|
#[arg(short = 'n', long = "max-count")]
|
|
limit: Option<usize>,
|
|
#[arg(short = '1')]
|
|
one: bool,
|
|
#[arg(long)]
|
|
format: Option<String>,
|
|
#[arg(long)]
|
|
pretty: Option<String>,
|
|
#[arg(short = 'p')]
|
|
patch: bool,
|
|
#[arg(long)]
|
|
stat: bool,
|
|
revision: Option<String>,
|
|
#[arg(last = true)]
|
|
paths: Vec<String>,
|
|
},
|
|
Diff {
|
|
#[arg(long)]
|
|
check: bool,
|
|
#[arg(long)]
|
|
numstat: bool,
|
|
#[arg(long)]
|
|
no_renames: bool,
|
|
#[arg(long)]
|
|
diff_filter: Option<String>,
|
|
#[arg(short = 'z')]
|
|
nul: bool,
|
|
#[arg(long, alias = "staged")]
|
|
cached: bool,
|
|
#[arg(long)]
|
|
stat: bool,
|
|
#[arg(long)]
|
|
name_only: bool,
|
|
#[arg(long)]
|
|
name_status: bool,
|
|
revisions: Vec<String>,
|
|
#[arg(last = true)]
|
|
paths: Vec<String>,
|
|
},
|
|
Branch {
|
|
#[arg(long)]
|
|
list: bool,
|
|
#[arg(long)]
|
|
show_current: bool,
|
|
#[arg(short = 'a')]
|
|
all: bool,
|
|
#[arg(short = 'd')]
|
|
delete: bool,
|
|
#[arg(short = 'D')]
|
|
force_delete: bool,
|
|
#[arg(short = 'f', long)]
|
|
force: bool,
|
|
#[arg(short = 'm')]
|
|
rename: bool,
|
|
name: Option<String>,
|
|
start: Option<String>,
|
|
},
|
|
Switch {
|
|
#[arg(short = 'c')]
|
|
create: bool,
|
|
target: String,
|
|
},
|
|
Checkout {
|
|
#[arg(long)]
|
|
detach: bool,
|
|
#[arg(long, short = 'f')]
|
|
force: bool,
|
|
#[arg(short = 'b')]
|
|
create: bool,
|
|
target: Option<String>,
|
|
paths: Vec<String>,
|
|
},
|
|
Merge {
|
|
#[arg(long)]
|
|
no_edit: bool,
|
|
#[arg(long)]
|
|
ff_only: bool,
|
|
#[arg(long)]
|
|
no_ff: bool,
|
|
#[arg(long)]
|
|
no_commit: bool,
|
|
#[arg(long)]
|
|
abort: bool,
|
|
#[arg(short = 'm')]
|
|
message: Option<String>,
|
|
revision: Option<String>,
|
|
},
|
|
Rebase {
|
|
#[arg(long)]
|
|
abort: bool,
|
|
#[arg(long = "continue")]
|
|
resume: bool,
|
|
#[arg(long)]
|
|
skip: bool,
|
|
upstream: Option<String>,
|
|
},
|
|
Worktree {
|
|
#[command(subcommand)]
|
|
command: Worktree,
|
|
},
|
|
LsFiles {
|
|
#[arg(long)]
|
|
others: bool,
|
|
#[arg(long)]
|
|
exclude_standard: bool,
|
|
#[arg(short = 'z')]
|
|
nul: bool,
|
|
},
|
|
Reset {
|
|
#[arg(long, conflicts_with_all = ["hard", "mixed"])]
|
|
soft: bool,
|
|
#[arg(long, conflicts_with = "mixed")]
|
|
hard: bool,
|
|
#[arg(long)]
|
|
mixed: bool,
|
|
revision: Option<String>,
|
|
paths: Vec<String>,
|
|
},
|
|
Restore {
|
|
#[arg(long)]
|
|
staged: bool,
|
|
paths: Vec<String>,
|
|
},
|
|
Stash {
|
|
#[arg(short = 'm', long)]
|
|
message: Option<String>,
|
|
command: Option<String>,
|
|
entry: Option<String>,
|
|
},
|
|
Tag {
|
|
#[arg(short = 'a')]
|
|
annotated: bool,
|
|
#[arg(short = 'm')]
|
|
message: Option<String>,
|
|
#[arg(short = 'l', long)]
|
|
list: bool,
|
|
#[arg(short = 'd')]
|
|
delete: bool,
|
|
name: Option<String>,
|
|
revision: Option<String>,
|
|
},
|
|
Remote {
|
|
#[arg(short = 'v', long)]
|
|
verbose: bool,
|
|
command: Option<String>,
|
|
name: Option<String>,
|
|
url: Option<String>,
|
|
},
|
|
CherryPick {
|
|
#[arg(long = "continue")]
|
|
resume: bool,
|
|
#[arg(long)]
|
|
abort: bool,
|
|
revision: Option<String>,
|
|
},
|
|
}
|
|
#[derive(Subcommand)]
|
|
enum Worktree {
|
|
Prune,
|
|
List {
|
|
#[arg(long)]
|
|
porcelain: bool,
|
|
},
|
|
Add {
|
|
#[arg(long)]
|
|
detach: bool,
|
|
#[arg(short = 'b')]
|
|
branch: Option<String>,
|
|
path: String,
|
|
revision: Option<String>,
|
|
},
|
|
Remove {
|
|
#[arg(long)]
|
|
force: bool,
|
|
path: String,
|
|
},
|
|
}
|
|
fn required(value: Option<String>, what: &str) -> Result<String> {
|
|
value.ok_or_else(|| error(what, "argument required"))
|
|
}
|
|
pub fn run(cli: Cli) -> Result<i32> {
|
|
let quiet = cli.quiet;
|
|
let count = std::env::var("GIT_CONFIG_COUNT")
|
|
.ok()
|
|
.and_then(|s| s.parse::<usize>().ok())
|
|
.unwrap_or(0);
|
|
for (i, value) in cli.config.iter().enumerate() {
|
|
let (key, value) = value.split_once('=').unwrap_or((value, "true"));
|
|
// WASI has one thread. Supply Git's per-command configuration before discovery.
|
|
unsafe {
|
|
std::env::set_var(format!("GIT_CONFIG_KEY_{}", count + i), key);
|
|
std::env::set_var(format!("GIT_CONFIG_VALUE_{}", count + i), value);
|
|
}
|
|
}
|
|
if !cli.config.is_empty() {
|
|
unsafe {
|
|
std::env::set_var("GIT_CONFIG_COUNT", (count + cli.config.len()).to_string());
|
|
}
|
|
}
|
|
if let Some(dir) = cli.directory {
|
|
std::env::set_current_dir(dir).map_err(|e| error("chdir", e))?;
|
|
}
|
|
if let Command::Init {
|
|
bare,
|
|
initial_branch,
|
|
path,
|
|
} = cli.command
|
|
{
|
|
let path = path.unwrap_or_else(|| ".".into());
|
|
let existing = std::path::Path::new(&path)
|
|
.join(if bare { "HEAD" } else { ".git/HEAD" })
|
|
.exists();
|
|
let repo = if existing {
|
|
repo::open(&path)
|
|
} else {
|
|
let repo = if bare {
|
|
gix::init_bare(&path)
|
|
} else {
|
|
gix::init(&path)
|
|
}
|
|
.map_err(|e| error("init", e))?;
|
|
std::fs::write(
|
|
repo.git_dir().join("HEAD"),
|
|
format!("ref: refs/heads/{initial_branch}\n"),
|
|
)
|
|
.map_err(|e| error("init HEAD", e))?;
|
|
Ok(repo)
|
|
}?;
|
|
if !quiet {
|
|
println!(
|
|
"{} Git repository in {}/",
|
|
if existing {
|
|
"Reinitialized existing"
|
|
} else {
|
|
"Initialized empty"
|
|
},
|
|
repo.git_dir()
|
|
.canonicalize()
|
|
.map_err(|e| error("init", e))?
|
|
.display()
|
|
);
|
|
}
|
|
return Ok(0);
|
|
}
|
|
if let Command::Clone { url, path } = cli.command {
|
|
let path = path.unwrap_or_else(|| {
|
|
url.trim_end_matches('/')
|
|
.rsplit('/')
|
|
.next()
|
|
.unwrap_or("repo")
|
|
.trim_end_matches(".git")
|
|
.to_owned()
|
|
});
|
|
network::clone(&url, &path)?;
|
|
return Ok(0);
|
|
}
|
|
let discovered = gix::discover(".").map_err(|e| error("repository", e))?;
|
|
let root = discovered
|
|
.workdir()
|
|
.unwrap_or(discovered.git_dir())
|
|
.to_owned();
|
|
std::env::set_current_dir(&root).map_err(|e| error("chdir", e))?;
|
|
let path = root.to_string_lossy().into_owned();
|
|
let repository = repo::open(&path)?;
|
|
match cli.command {
|
|
Command::Reset {
|
|
soft,
|
|
hard,
|
|
revision,
|
|
paths,
|
|
..
|
|
} => local::reset(
|
|
&repository,
|
|
revision.as_deref().unwrap_or("HEAD"),
|
|
&paths,
|
|
soft,
|
|
hard,
|
|
quiet,
|
|
)?,
|
|
Command::Restore { staged, paths } => local::restore(
|
|
&repository,
|
|
if staged { Some("HEAD") } else { None },
|
|
&paths,
|
|
staged,
|
|
)?,
|
|
Command::Stash {
|
|
command,
|
|
message,
|
|
entry,
|
|
} => {
|
|
return stash::run(
|
|
&repository,
|
|
command.as_deref().unwrap_or("push"),
|
|
message,
|
|
entry,
|
|
quiet,
|
|
);
|
|
}
|
|
Command::Tag {
|
|
annotated,
|
|
message,
|
|
delete,
|
|
name,
|
|
revision,
|
|
..
|
|
} => local::tag(&repository, name, revision, annotated, message, delete)?,
|
|
Command::Remote {
|
|
verbose,
|
|
command,
|
|
name,
|
|
url,
|
|
} => match command.as_deref() {
|
|
Some("add") | Some("set-url") => {
|
|
remotes::set(
|
|
&repository,
|
|
&required(name, "remote name")?,
|
|
&required(url, "remote URL")?,
|
|
command.as_deref() == Some("add"),
|
|
)?;
|
|
}
|
|
Some("remove") => remotes::remove(&repository, &required(name, "remote name")?)?,
|
|
Some("get-url") => {
|
|
let name = required(name, "remote name")?;
|
|
let remote = remotes::list(&repository)?
|
|
.into_iter()
|
|
.find(|r| r.name == name)
|
|
.ok_or_else(|| error("remote", "remote does not exist"))?;
|
|
println!("{}", remote.url.unwrap_or_default());
|
|
}
|
|
None => {
|
|
for remote in remotes::list(&repository)? {
|
|
if verbose {
|
|
let url = remote.url.unwrap_or_default();
|
|
println!(
|
|
"{}\t{} (fetch)\n{}\t{} (push)",
|
|
remote.name, url, remote.name, url
|
|
);
|
|
} else {
|
|
println!("{}", remote.name);
|
|
}
|
|
}
|
|
}
|
|
_ => {
|
|
eprintln!("usage: git remote [add|remove|set-url|get-url]");
|
|
return Ok(129);
|
|
}
|
|
},
|
|
Command::CherryPick {
|
|
resume,
|
|
abort,
|
|
revision,
|
|
} => return cherry_pick::run(&repository, revision, resume, abort, quiet),
|
|
Command::MergeBase { a, b, is_ancestor } => {
|
|
return plumbing::merge_base(&repository, &a, &b, is_ancestor);
|
|
}
|
|
Command::RevList { count, revisions } => {
|
|
let mut included = std::collections::BTreeSet::new();
|
|
let mut excluded = std::collections::BTreeSet::new();
|
|
for revision in revisions {
|
|
let (tip, hidden) = plumbing::range(&repository, &revision)?;
|
|
included.extend(plumbing::ancestors(&repository, tip)?);
|
|
excluded.extend(hidden);
|
|
}
|
|
included.retain(|id| !excluded.contains(id));
|
|
if count {
|
|
println!("{}", included.len());
|
|
} else {
|
|
for id in included {
|
|
println!("{id}");
|
|
}
|
|
}
|
|
}
|
|
Command::SymbolicRef { short, name } => {
|
|
let reference = repository
|
|
.find_reference(name.as_str())
|
|
.map_err(|e| error("symbolic-ref", e))?;
|
|
if let gix::refs::TargetRef::Symbolic(target) = reference.target() {
|
|
println!(
|
|
"{}",
|
|
if short {
|
|
target.shorten().to_string()
|
|
} else {
|
|
target.as_bstr().to_string()
|
|
}
|
|
);
|
|
} else {
|
|
if !quiet {
|
|
eprintln!("fatal: ref {name} is not a symbolic ref");
|
|
}
|
|
return Ok(if quiet { 1 } else { 128 });
|
|
}
|
|
}
|
|
Command::UpdateRef { name, new, old } => {
|
|
plumbing::update_ref(&repository, &name, &new, old.as_deref())?
|
|
}
|
|
Command::CheckIgnore { no_index, paths } => {
|
|
return plumbing::check_ignore(&repository, &paths, no_index);
|
|
}
|
|
Command::Clean { force, directories } => {
|
|
if !force {
|
|
return Err(error("clean", "requires -f"));
|
|
}
|
|
plumbing::clean(&repository, directories, quiet)?;
|
|
}
|
|
Command::LsFiles {
|
|
others,
|
|
exclude_standard: _,
|
|
nul,
|
|
} => {
|
|
if others {
|
|
for path in changes::status(&repository)?.untracked {
|
|
print!("{path}{}", if nul { "\0" } else { "\n" });
|
|
}
|
|
return Ok(0);
|
|
}
|
|
let index = repository
|
|
.index_or_empty()
|
|
.map_err(|e| error("ls-files", e))?;
|
|
for entry in index.entries() {
|
|
print!("{}{}", entry.path(&index), if nul { "\0" } else { "\n" });
|
|
}
|
|
}
|
|
Command::Config { key, value, .. } => {
|
|
let file = repository.common_dir().join("config");
|
|
if let Some(value) = value {
|
|
let mut config = gix::config::File::from_path_no_includes(
|
|
file.clone(),
|
|
gix::config::Source::Local,
|
|
)
|
|
.map_err(|e| error("config", e))?;
|
|
config
|
|
.set_raw_value(&key, value.as_bytes().as_bstr())
|
|
.map_err(|e| error("config", e))?;
|
|
let mut output = std::fs::File::create(file).map_err(|e| error("config", e))?;
|
|
config
|
|
.write_to(&mut output)
|
|
.map_err(|e| error("config", e))?;
|
|
} else if let Some(value) = repository.config_snapshot().string(&key) {
|
|
println!("{value}");
|
|
} else {
|
|
return Ok(1);
|
|
}
|
|
}
|
|
Command::RevParse {
|
|
verify,
|
|
show_toplevel,
|
|
git_dir,
|
|
abbrev_ref,
|
|
short,
|
|
revisions,
|
|
} => {
|
|
if show_toplevel {
|
|
println!("{}", root.display());
|
|
} else if git_dir {
|
|
println!(".git");
|
|
} else if abbrev_ref {
|
|
println!(
|
|
"{}",
|
|
head_info(&repository)?
|
|
.branch
|
|
.unwrap_or_else(|| "HEAD".into())
|
|
.trim_start_matches("refs/heads/")
|
|
);
|
|
} else {
|
|
for revision in revisions {
|
|
let id = match repository.rev_parse_single(revision.as_str()) {
|
|
Ok(id) => id.to_string(),
|
|
Err(_) if verify && quiet => return Ok(1),
|
|
Err(e) => return Err(error("revision", e)),
|
|
};
|
|
println!("{}", if short { &id[..7] } else { &id });
|
|
}
|
|
}
|
|
}
|
|
Command::Add { all, mut paths } => {
|
|
let all = all || paths.iter().any(|p| p == ".");
|
|
paths.retain(|p| p != ".");
|
|
staging::add(
|
|
&repository,
|
|
staging::AddParams {
|
|
repo: path,
|
|
paths,
|
|
all: Some(all),
|
|
},
|
|
)?;
|
|
}
|
|
Command::Rm { cached, paths } => {
|
|
let result = staging::rm(
|
|
&repository,
|
|
staging::RmParams {
|
|
repo: path,
|
|
paths,
|
|
keep_file: Some(cached),
|
|
},
|
|
)?;
|
|
for path in result.paths {
|
|
println!("rm '{path}'");
|
|
}
|
|
}
|
|
Command::Commit {
|
|
message,
|
|
file,
|
|
amend,
|
|
no_edit,
|
|
all,
|
|
allow_empty,
|
|
..
|
|
} => {
|
|
let before = head_info(&repository)?;
|
|
let message = if let Some(file) = file {
|
|
std::fs::read_to_string(file).map_err(|e| error("commit message", e))?
|
|
} else if let Some(message) = message {
|
|
message
|
|
} else if amend && no_edit {
|
|
repository
|
|
.head_commit()
|
|
.map_err(|e| error("amend", e))?
|
|
.decode()
|
|
.map_err(|e| error("amend message", e))?
|
|
.message
|
|
.to_str_lossy()
|
|
.into_owned()
|
|
} else {
|
|
eprintln!("usage: git commit -m <message> | -F <file> | --amend --no-edit");
|
|
return Ok(129);
|
|
};
|
|
let message = plumbing::trailer(&message);
|
|
if !amend && !allow_empty && merge::pending_parent(&repository)?.is_none() {
|
|
let status = changes::status(&repository)?;
|
|
if status.staged.is_empty()
|
|
&& (!all || status.unstaged.is_empty())
|
|
&& status.conflicted.is_empty()
|
|
{
|
|
output::long_status(&status);
|
|
return Ok(1);
|
|
}
|
|
}
|
|
let result = staging::commit(
|
|
&repository,
|
|
staging::CommitParams {
|
|
repo: path.clone(),
|
|
message: format!("{message}\n"),
|
|
author_name: None,
|
|
author_email: None,
|
|
all: Some(all),
|
|
amend: Some(amend),
|
|
allow_empty: Some(allow_empty),
|
|
},
|
|
)?;
|
|
if quiet {
|
|
return Ok(0);
|
|
}
|
|
println!(
|
|
"[{}{} {}] {}",
|
|
before
|
|
.branch
|
|
.unwrap_or_else(|| "HEAD".into())
|
|
.trim_start_matches("refs/heads/"),
|
|
if before.unborn { " (root-commit)" } else { "" },
|
|
&result.id[..7],
|
|
message.lines().next().unwrap_or("")
|
|
);
|
|
if amend {
|
|
output::author_date(
|
|
&repository
|
|
.find_commit(
|
|
gix::ObjectId::from_hex(result.id.as_bytes())
|
|
.map_err(|e| error("amend", e))?,
|
|
)
|
|
.map_err(|e| error("amend", e))?,
|
|
)?;
|
|
}
|
|
let old = match if amend {
|
|
repository
|
|
.find_commit(
|
|
gix::ObjectId::from_hex(result.id.as_bytes())
|
|
.map_err(|e| error("amend", e))?,
|
|
)
|
|
.map_err(|e| error("amend", e))?
|
|
.parent_ids()
|
|
.next()
|
|
.map(|id| id.to_string())
|
|
} else {
|
|
before.id
|
|
} {
|
|
Some(id) => id,
|
|
None => repository
|
|
.write_object(&gix::objs::Tree { entries: vec![] })
|
|
.map_err(|e| error("commit summary", e))?
|
|
.to_string(),
|
|
};
|
|
output::commit_stat(&changes::diff(changes::DiffParams {
|
|
repo: path,
|
|
from: Some(old),
|
|
to: Some(result.id),
|
|
staged: None,
|
|
path: None,
|
|
context: None,
|
|
})?);
|
|
}
|
|
Command::Status {
|
|
untracked_files,
|
|
nul,
|
|
short,
|
|
porcelain,
|
|
branch,
|
|
} => {
|
|
let mut status = changes::refresh_status(&repository)?;
|
|
let index = repository
|
|
.index_or_empty()
|
|
.map_err(|e| error("status index", e))?;
|
|
let tracked: Vec<_> = index
|
|
.entries()
|
|
.iter()
|
|
.map(|e| e.path(&index).to_str_lossy().into_owned())
|
|
.collect();
|
|
let mut untracked = std::collections::BTreeSet::new();
|
|
for path in status.untracked {
|
|
if untracked_files == "no" {
|
|
continue;
|
|
}
|
|
if untracked_files == "all" {
|
|
untracked.insert(path);
|
|
continue;
|
|
}
|
|
let collapsed = path
|
|
.match_indices('/')
|
|
.map(|(i, _)| &path[..=i])
|
|
.find(|prefix| !tracked.iter().any(|t| t.starts_with(*prefix)))
|
|
.unwrap_or(&path);
|
|
untracked.insert(collapsed.to_owned());
|
|
}
|
|
status.untracked = untracked.into_iter().collect();
|
|
if !short && porcelain.is_none() {
|
|
output::long_status(&status);
|
|
return Ok(0);
|
|
}
|
|
if branch {
|
|
println!(
|
|
"## {}",
|
|
if status.head.detached {
|
|
"HEAD (no branch)".into()
|
|
} else {
|
|
let name = status
|
|
.head
|
|
.branch
|
|
.as_deref()
|
|
.unwrap_or("HEAD")
|
|
.trim_start_matches("refs/heads/");
|
|
if status.head.unborn {
|
|
format!("No commits yet on {name}")
|
|
} else {
|
|
name.to_owned()
|
|
}
|
|
}
|
|
);
|
|
}
|
|
let mut rows = std::collections::BTreeMap::new();
|
|
let mut sources = std::collections::BTreeMap::new();
|
|
for c in status.staged {
|
|
if let Some(source) = c.old_path {
|
|
sources.insert(c.path.clone(), source);
|
|
}
|
|
rows.entry(c.path).or_insert([' '; 2])[0] = code(&c.status);
|
|
}
|
|
for c in status.unstaged {
|
|
rows.entry(c.path).or_insert([' '; 2])[1] = code(&c.status);
|
|
}
|
|
for p in status.untracked {
|
|
rows.insert(p, ['?', '?']);
|
|
}
|
|
for p in status.conflicted {
|
|
rows.insert(p, ['U', 'U']);
|
|
}
|
|
for untracked in [false, true] {
|
|
for (p, c) in &rows {
|
|
if (c == &['?', '?']) == untracked {
|
|
if let Some(source) = sources.get(p) {
|
|
if nul {
|
|
print!("{}{} {p}\0{source}\0", c[0], c[1]);
|
|
} else {
|
|
println!("{}{} {source} -> {p}", c[0], c[1]);
|
|
}
|
|
} else {
|
|
print!("{}{} {p}{}", c[0], c[1], if nul { "\0" } else { "\n" });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Command::Diff {
|
|
check,
|
|
numstat,
|
|
no_renames,
|
|
diff_filter,
|
|
nul,
|
|
cached,
|
|
stat,
|
|
name_only,
|
|
name_status,
|
|
revisions,
|
|
paths,
|
|
} => {
|
|
let (from, to) = if revisions.len() == 1 && revisions[0].contains("..") {
|
|
let (a, b) = revisions[0].split_once("..").unwrap();
|
|
(Some(a.into()), Some(b.into()))
|
|
} else {
|
|
(revisions.first().cloned(), revisions.get(1).cloned())
|
|
};
|
|
if diff_filter.as_deref() == Some("U") {
|
|
for p in changes::status(&repository)?.conflicted {
|
|
print!("{p}{}", if nul { "\0" } else { "\n" });
|
|
}
|
|
return Ok(0);
|
|
}
|
|
let diffs = changes::diff_with_renames(
|
|
changes::DiffParams {
|
|
repo: path,
|
|
from,
|
|
to: to.clone(),
|
|
staged: Some(cached),
|
|
path: paths.first().cloned(),
|
|
context: None,
|
|
},
|
|
!no_renames,
|
|
)?;
|
|
if check {
|
|
return changes::check(&repository, &diffs, to.as_deref(), cached);
|
|
}
|
|
if name_only || name_status {
|
|
for diff in diffs {
|
|
if name_status {
|
|
print!(
|
|
"{}{}{}{}",
|
|
code(&diff.status),
|
|
if nul { "\0" } else { "\t" },
|
|
diff.path,
|
|
if nul { "\0" } else { "\n" }
|
|
);
|
|
} else {
|
|
print!("{}{}", diff.path, if nul { "\0" } else { "\n" });
|
|
}
|
|
}
|
|
} else if numstat {
|
|
for diff in &diffs {
|
|
output::numstat(diff, nul);
|
|
}
|
|
} else if stat {
|
|
output::stat(&diffs, false);
|
|
} else {
|
|
for diff in diffs {
|
|
print!("{}", diff.patch);
|
|
}
|
|
}
|
|
}
|
|
Command::Log {
|
|
decorate,
|
|
name_only,
|
|
no_renames,
|
|
diff_merges,
|
|
oneline,
|
|
limit,
|
|
one,
|
|
format,
|
|
pretty,
|
|
patch,
|
|
stat,
|
|
revision,
|
|
paths,
|
|
} => {
|
|
let limit = if one { 1 } else { limit.unwrap_or(usize::MAX) };
|
|
let (tip, excluded) =
|
|
plumbing::range(&repository, revision.as_deref().unwrap_or("HEAD"))?;
|
|
let commit = repository.find_commit(tip).map_err(|e| error("log", e))?;
|
|
let ids = if let Some(path) = paths.first() {
|
|
history::paths(&repository, commit.id, path)?
|
|
} else {
|
|
commit
|
|
.ancestors()
|
|
.use_commit_graph(false)
|
|
.sorting(gix::revision::walk::Sorting::ByCommitTime(
|
|
Default::default(),
|
|
))
|
|
.all()
|
|
.map_err(|e| error("log", e))?
|
|
.filter(|info| {
|
|
info.as_ref()
|
|
.map_or(true, |info| !excluded.contains(&info.id))
|
|
})
|
|
.take(limit)
|
|
.map(|info| info.map(|info| info.id).map_err(|e| error("log", e)))
|
|
.collect::<Result<Vec<_>>>()?
|
|
};
|
|
let mut first = true;
|
|
for id in ids
|
|
.into_iter()
|
|
.filter(|id| !excluded.contains(id))
|
|
.take(limit)
|
|
{
|
|
let c = repository.find_commit(id).map_err(|e| error("log", e))?;
|
|
let decoded = c.decode().map_err(|e| error("log", e))?;
|
|
let message = decoded.message.to_str_lossy();
|
|
let diffs = if (patch || stat || name_only || !paths.is_empty())
|
|
&& (decoded.parents.len() <= 1
|
|
|| diff_merges.as_deref() == Some("first-parent")
|
|
|| !name_only)
|
|
{
|
|
let old = match decoded.parents.first() {
|
|
Some(id) => id.to_string(),
|
|
None => repository
|
|
.write_object(&gix::objs::Tree { entries: vec![] })
|
|
.map_err(|e| error("log tree", e))?
|
|
.to_string(),
|
|
};
|
|
changes::diff_with_renames(
|
|
changes::DiffParams {
|
|
repo: path.clone(),
|
|
from: Some(old),
|
|
to: Some(c.id.to_string()),
|
|
staged: None,
|
|
path: paths.first().cloned(),
|
|
context: None,
|
|
},
|
|
!no_renames,
|
|
)?
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
if let Some(template) = format.as_deref().or(pretty
|
|
.as_deref()
|
|
.map(|s| s.strip_prefix("format:").unwrap_or(s)))
|
|
{
|
|
let text = pretty::format(&c, template)?;
|
|
if pretty.is_some() {
|
|
if !first {
|
|
println!();
|
|
}
|
|
print!("{text}");
|
|
} else {
|
|
println!("{text}");
|
|
}
|
|
first = false;
|
|
} else if oneline {
|
|
println!(
|
|
"{}{} {}",
|
|
&c.id.to_string()[..7],
|
|
if decorate.is_some() {
|
|
pretty::decorations(&c)?
|
|
} else {
|
|
String::new()
|
|
},
|
|
message.lines().next().unwrap_or("")
|
|
);
|
|
} else {
|
|
if !first {
|
|
println!();
|
|
}
|
|
if decorate.is_some() {
|
|
output::decorated_commit(&c, &pretty::decorations(&c)?)?;
|
|
} else {
|
|
output::commit(&c)?;
|
|
}
|
|
first = false;
|
|
}
|
|
if name_only && !diffs.is_empty() {
|
|
println!();
|
|
for diff in &diffs {
|
|
println!("{}", diff.path);
|
|
}
|
|
}
|
|
if patch || stat {
|
|
println!();
|
|
if stat {
|
|
output::stat(&diffs, false);
|
|
}
|
|
if patch {
|
|
for diff in diffs {
|
|
print!("{}", diff.patch);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Command::Branch {
|
|
list,
|
|
delete,
|
|
force_delete,
|
|
force,
|
|
show_current,
|
|
all,
|
|
rename,
|
|
name,
|
|
start,
|
|
} => {
|
|
if rename {
|
|
let new = required(start.clone().or(name.clone()), "new branch name")?;
|
|
local::rename_branch(
|
|
&repository,
|
|
if start.is_some() {
|
|
name.as_deref()
|
|
} else {
|
|
None
|
|
},
|
|
&new,
|
|
)?;
|
|
return Ok(0);
|
|
}
|
|
if show_current {
|
|
if let Some(branch) = head_info(&repository)?.branch {
|
|
println!("{}", branch.trim_start_matches("refs/heads/"));
|
|
}
|
|
return Ok(0);
|
|
}
|
|
if let Some(name) = name.as_ref().filter(|_| !list) {
|
|
let name = name.clone();
|
|
if delete || force_delete {
|
|
let id = branches::resolve(&repository, &name)?.to_string();
|
|
branches::delete(
|
|
&repository,
|
|
branches::BranchDeleteParams {
|
|
repo: path,
|
|
name: name.clone(),
|
|
force: Some(force_delete),
|
|
},
|
|
)?;
|
|
if !quiet {
|
|
println!("Deleted branch {name} (was {}).", &id[..7]);
|
|
}
|
|
} else if force {
|
|
let full = branches::branch_name(&name)?;
|
|
worktrees::ensure_available(&repository, &full)?;
|
|
if head_info(&repository)?.branch.as_deref() == Some(&full) {
|
|
return Err(error("branch", "cannot force update current branch"));
|
|
}
|
|
let id = branches::resolve(&repository, start.as_deref().unwrap_or("HEAD"))?;
|
|
plumbing::update_ref(&repository, &full, &id.to_string(), None)?;
|
|
} else {
|
|
branches::create(
|
|
&repository,
|
|
branches::BranchCreateParams {
|
|
repo: path,
|
|
name,
|
|
from: start,
|
|
checkout: Some(false),
|
|
},
|
|
)?;
|
|
}
|
|
} else {
|
|
let pattern = name.as_deref();
|
|
let head = head_info(&repository)?;
|
|
for branch in repository
|
|
.references()
|
|
.map_err(|e| error("branch", e))?
|
|
.all()
|
|
.map_err(|e| error("branch", e))?
|
|
{
|
|
let branch = branch.map_err(|e| error("branch", e))?;
|
|
let name = branch.name().as_bstr().to_str_lossy();
|
|
if !name.starts_with("refs/heads/")
|
|
&& !(all && name.starts_with("refs/remotes/"))
|
|
{
|
|
continue;
|
|
}
|
|
if list
|
|
&& pattern
|
|
.is_some_and(|pattern| branch.name().shorten().to_string() != pattern)
|
|
{
|
|
continue;
|
|
}
|
|
println!(
|
|
"{} {}",
|
|
if head.branch.as_deref()
|
|
== Some(branch.name().as_bstr().to_str_lossy().as_ref())
|
|
{
|
|
"*"
|
|
} else {
|
|
" "
|
|
},
|
|
if name.starts_with("refs/remotes/") {
|
|
format!("remotes/{}", name.trim_start_matches("refs/remotes/"))
|
|
} else {
|
|
branch.name().shorten().to_string()
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Command::Checkout {
|
|
detach,
|
|
force,
|
|
create,
|
|
target,
|
|
mut paths,
|
|
} => {
|
|
if !create
|
|
&& (!paths.is_empty()
|
|
|| target.as_ref().is_some_and(|t| {
|
|
root.join(t).is_file()
|
|
&& repository
|
|
.try_find_reference(format!("refs/heads/{t}").as_str())
|
|
.ok()
|
|
.flatten()
|
|
.is_none()
|
|
}))
|
|
{
|
|
if let Some(target) = target {
|
|
paths.insert(0, target);
|
|
}
|
|
local::restore(&repository, None, &paths, false)?;
|
|
} else {
|
|
branches::checkout(
|
|
&repository,
|
|
branches::CheckoutParams {
|
|
repo: path,
|
|
target: if detach {
|
|
branches::resolve(&repository, &required(target, "checkout target")?)?
|
|
.to_string()
|
|
} else {
|
|
required(target, "checkout target")?
|
|
},
|
|
create: Some(create),
|
|
force: Some(force),
|
|
},
|
|
)?;
|
|
}
|
|
}
|
|
Command::Switch { create, target } => {
|
|
branches::checkout(
|
|
&repository,
|
|
branches::CheckoutParams {
|
|
repo: path,
|
|
target,
|
|
create: Some(create),
|
|
force: None,
|
|
},
|
|
)?;
|
|
}
|
|
Command::Merge {
|
|
no_edit: _,
|
|
ff_only,
|
|
no_ff,
|
|
no_commit,
|
|
abort,
|
|
message,
|
|
revision,
|
|
} => {
|
|
let old = head_info(&repository)?.id;
|
|
let message =
|
|
message.or_else(|| revision.as_ref().map(|r| format!("Merge branch '{r}'\n")));
|
|
let r = if abort {
|
|
merge::abort(&repository)?
|
|
} else {
|
|
merge::merge(
|
|
&repository,
|
|
merge::MergeParams {
|
|
repo: path.clone(),
|
|
rev: required(revision, "merge")?,
|
|
message: message.map(|m| format!("{}\n", m.trim_end())),
|
|
ff: Some(if ff_only {
|
|
merge::FastForward::Only
|
|
} else if no_ff {
|
|
merge::FastForward::Never
|
|
} else {
|
|
merge::FastForward::Allow
|
|
}),
|
|
no_commit: Some(no_commit),
|
|
},
|
|
)?
|
|
};
|
|
if r.already_up_to_date && !quiet {
|
|
println!("Already up to date.");
|
|
}
|
|
output::conflicts(&r.conflicts, true);
|
|
if quiet {
|
|
return Ok(if r.conflicts.is_empty() { 0 } else { 1 });
|
|
}
|
|
if !r.conflicts.is_empty() {
|
|
return Ok(1);
|
|
}
|
|
if !abort && !r.already_up_to_date && !r.pending {
|
|
let new = head_info(&repo::open(&path)?)?.id.unwrap();
|
|
if let Some(old) = old {
|
|
if r.fast_forward {
|
|
println!("Updating {}..{}\nFast-forward", &old[..7], &new[..7]);
|
|
} else {
|
|
println!("Merge made by the 'ort' strategy.");
|
|
}
|
|
output::stat(
|
|
&changes::diff(changes::DiffParams {
|
|
repo: path,
|
|
from: Some(old),
|
|
to: Some(new),
|
|
staged: None,
|
|
path: None,
|
|
context: None,
|
|
})?,
|
|
true,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Command::Rebase {
|
|
abort,
|
|
resume,
|
|
skip,
|
|
upstream,
|
|
} => {
|
|
let r = if abort {
|
|
rebase::abort(&repository)?
|
|
} else if resume || skip {
|
|
rebase::resume(&repository, skip)?
|
|
} else {
|
|
rebase::start(
|
|
&repository,
|
|
rebase::RebaseParams {
|
|
repo: path.clone(),
|
|
onto: Some(required(upstream, "rebase")?),
|
|
branch: None,
|
|
},
|
|
)?
|
|
};
|
|
if quiet {
|
|
output::conflicts(&r.conflicts, false);
|
|
return Ok(if r.conflicts.is_empty() { 0 } else { 1 });
|
|
}
|
|
if let Some(id) = &r.resolved {
|
|
let commit = repository
|
|
.find_commit(
|
|
gix::ObjectId::from_hex(id.as_bytes())
|
|
.map_err(|e| error("rebase output", e))?,
|
|
)
|
|
.map_err(|e| error("rebase output", e))?;
|
|
let decoded = commit.decode().map_err(|e| error("rebase output", e))?;
|
|
println!(
|
|
"[detached HEAD {}] {}",
|
|
&id[..7],
|
|
decoded.message.to_str_lossy().lines().next().unwrap_or("")
|
|
);
|
|
output::commit_stat(&changes::diff(changes::DiffParams {
|
|
repo: path,
|
|
from: Some(decoded.parents[0].to_string()),
|
|
to: Some(id.clone()),
|
|
staged: None,
|
|
path: None,
|
|
context: None,
|
|
})?);
|
|
}
|
|
output::conflicts(&r.conflicts, false);
|
|
if !r.conflicts.is_empty() {
|
|
return Ok(1);
|
|
}
|
|
}
|
|
Command::Worktree { command } => match command {
|
|
Worktree::Prune => worktrees::prune(&repository)?,
|
|
Worktree::List { porcelain } => {
|
|
let common = repository
|
|
.common_dir()
|
|
.canonicalize()
|
|
.map_err(|e| error("worktree list", e))?;
|
|
let mut paths = vec![
|
|
common
|
|
.parent()
|
|
.ok_or_else(|| error("worktree list", "no worktree"))?
|
|
.to_owned(),
|
|
];
|
|
if let Ok(admins) = std::fs::read_dir(common.join("worktrees")) {
|
|
let mut linked = Vec::new();
|
|
for admin in admins {
|
|
let admin = admin.map_err(|e| error("worktree list", e))?;
|
|
let gitfile = std::fs::read_to_string(admin.path().join("gitdir"))
|
|
.map_err(|e| error("worktree list", e))?;
|
|
if let Some(parent) = std::path::Path::new(gitfile.trim()).parent() {
|
|
linked.push(parent.to_owned());
|
|
}
|
|
}
|
|
linked.sort();
|
|
paths.extend(linked);
|
|
}
|
|
let width = paths
|
|
.iter()
|
|
.map(|p| p.to_string_lossy().len())
|
|
.max()
|
|
.unwrap_or(0)
|
|
+ 2;
|
|
for p in paths {
|
|
let p = p
|
|
.canonicalize()
|
|
.map_err(|e| error("worktree list path", e))?;
|
|
let linked = repo::open(&p.to_string_lossy())?;
|
|
let head = head_info(&linked)?;
|
|
let id = head.id.unwrap_or_else(|| "0".repeat(40));
|
|
if porcelain {
|
|
println!("worktree {}\nHEAD {}", p.display(), id);
|
|
if let Some(branch) = head.branch {
|
|
println!("branch {branch}");
|
|
} else {
|
|
println!("detached");
|
|
}
|
|
println!();
|
|
} else {
|
|
let branch = head
|
|
.branch
|
|
.map(|b| format!("[{}]", b.trim_start_matches("refs/heads/")))
|
|
.unwrap_or_else(|| "(detached HEAD)".into());
|
|
println!("{:width$}{} {}", p.display().to_string(), &id[..7], branch);
|
|
}
|
|
}
|
|
}
|
|
Worktree::Add {
|
|
detach,
|
|
branch,
|
|
path: target,
|
|
revision,
|
|
} => {
|
|
let branch = branch.unwrap_or_else(|| {
|
|
std::path::Path::new(&target)
|
|
.file_name()
|
|
.unwrap()
|
|
.to_string_lossy()
|
|
.into_owned()
|
|
});
|
|
let info = worktrees::add(
|
|
&repository,
|
|
worktrees::AddParams {
|
|
repo: path,
|
|
path: std::path::absolute(target)
|
|
.map_err(|e| error("worktree path", e))?
|
|
.to_string_lossy()
|
|
.into_owned(),
|
|
branch,
|
|
detached: Some(detach),
|
|
from: revision,
|
|
adopt: None,
|
|
},
|
|
)?;
|
|
let linked = repo::open(&info.path)?;
|
|
let commit = linked
|
|
.head_commit()
|
|
.map_err(|e| error("worktree HEAD", e))?;
|
|
let decoded = commit.decode().map_err(|e| error("worktree HEAD", e))?;
|
|
println!(
|
|
"HEAD is now at {} {}",
|
|
&commit.id.to_string()[..7],
|
|
decoded.message.to_str_lossy().lines().next().unwrap_or("")
|
|
);
|
|
}
|
|
Worktree::Remove { force, path } => {
|
|
let target = std::path::absolute(path)
|
|
.map_err(|e| error("worktree remove", e))?
|
|
.to_string_lossy()
|
|
.into_owned();
|
|
worktrees::remove(
|
|
&repo::open(&target)?,
|
|
worktrees::RemoveParams {
|
|
repo: target,
|
|
force: Some(force),
|
|
},
|
|
)?;
|
|
}
|
|
},
|
|
Command::Archive {
|
|
format: _,
|
|
output,
|
|
revision,
|
|
path,
|
|
} => {
|
|
archive::archive(&repository, &revision, path.as_deref(), output.as_deref())?;
|
|
}
|
|
Command::CatFile {
|
|
kind,
|
|
pretty: _,
|
|
size,
|
|
exists,
|
|
revision,
|
|
} => {
|
|
inspect::cat_file(&repository, &revision, kind, size, exists)?;
|
|
}
|
|
Command::LsTree {
|
|
recursive,
|
|
name_only,
|
|
revision,
|
|
path,
|
|
} => {
|
|
inspect::ls_tree(
|
|
&repository,
|
|
&revision,
|
|
recursive,
|
|
name_only,
|
|
path.as_deref(),
|
|
)?;
|
|
}
|
|
Command::Show {
|
|
no_patch,
|
|
format,
|
|
pretty,
|
|
revision,
|
|
stat,
|
|
oneline,
|
|
name_only,
|
|
} => {
|
|
let object = repository
|
|
.rev_parse_single(revision.as_deref().unwrap_or("HEAD"))
|
|
.map_err(|e| error("show", e))?
|
|
.object()
|
|
.map_err(|e| error("show", e))?;
|
|
if object.kind == gix::objs::Kind::Blob {
|
|
use std::io::Write;
|
|
std::io::stdout()
|
|
.write_all(&object.data)
|
|
.map_err(|e| error("show", e))?;
|
|
} else {
|
|
let commit = object.peel_to_commit().map_err(|e| error("show", e))?;
|
|
if no_patch {
|
|
if let Some(template) = format.as_deref().or(pretty
|
|
.as_deref()
|
|
.map(|s| s.strip_prefix("format:").unwrap_or(s)))
|
|
{
|
|
let text = pretty::format(&commit, template)?;
|
|
if pretty.is_some() && format.is_none() {
|
|
print!("{text}");
|
|
} else {
|
|
println!("{text}");
|
|
}
|
|
} else {
|
|
output::commit(&commit)?;
|
|
}
|
|
return Ok(0);
|
|
}
|
|
let merge_commit = commit.parent_ids().count() > 1;
|
|
if merge_commit && !stat {
|
|
let mut common: Option<std::collections::BTreeSet<String>> = None;
|
|
for parent in commit.parent_ids() {
|
|
let names = changes::diff(changes::DiffParams {
|
|
repo: path.clone(),
|
|
from: Some(parent.to_string()),
|
|
to: Some(commit.id.to_string()),
|
|
staged: None,
|
|
path: None,
|
|
context: None,
|
|
})?
|
|
.into_iter()
|
|
.map(|d| d.path)
|
|
.collect::<std::collections::BTreeSet<_>>();
|
|
common = Some(match common {
|
|
None => names,
|
|
Some(paths) => paths.intersection(&names).cloned().collect(),
|
|
});
|
|
}
|
|
if common.is_some_and(|paths| !paths.is_empty()) {
|
|
eprintln!(
|
|
"usage: git show: combined merge diffs are not supported (see README.md)"
|
|
);
|
|
return Ok(129);
|
|
}
|
|
}
|
|
if let Some(template) = format.as_deref().or(pretty
|
|
.as_deref()
|
|
.map(|s| s.strip_prefix("format:").unwrap_or(s)))
|
|
{
|
|
println!("{}\n", pretty::format(&commit, template)?);
|
|
} else if oneline {
|
|
let decoded = commit.decode().map_err(|e| error("show", e))?;
|
|
println!(
|
|
"{} {}",
|
|
&commit.id.to_string()[..7],
|
|
decoded.message.to_str_lossy().lines().next().unwrap_or("")
|
|
);
|
|
} else {
|
|
output::commit(&commit)?;
|
|
println!();
|
|
}
|
|
if merge_commit && !stat && !name_only {
|
|
return Ok(0);
|
|
}
|
|
let old = match commit.parent_ids().next() {
|
|
Some(id) => id.to_string(),
|
|
None => repository
|
|
.write_object(&gix::objs::Tree { entries: vec![] })
|
|
.map_err(|e| error("show", e))?
|
|
.to_string(),
|
|
};
|
|
let diffs = changes::diff(changes::DiffParams {
|
|
repo: path,
|
|
from: Some(old),
|
|
to: Some(commit.id.to_string()),
|
|
staged: None,
|
|
path: None,
|
|
context: None,
|
|
})?;
|
|
if name_only {
|
|
for diff in diffs {
|
|
println!("{}", diff.path);
|
|
}
|
|
} else if stat {
|
|
output::stat(&diffs, false);
|
|
} else {
|
|
for diff in diffs {
|
|
print!("{}", diff.patch);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Command::Fetch { remote, revision } => network::fetch(&path, &remote, revision.as_deref())?,
|
|
Command::Push {
|
|
set_upstream,
|
|
force,
|
|
remote,
|
|
refspec,
|
|
} => {
|
|
push::push(push::PushParams {
|
|
repo: path,
|
|
remote: Some(remote),
|
|
branch: None,
|
|
refspec,
|
|
force: Some(force),
|
|
set_upstream: Some(set_upstream),
|
|
delete: None,
|
|
})?;
|
|
}
|
|
Command::Init { .. } | Command::Clone { .. } => unreachable!(),
|
|
}
|
|
Ok(0)
|
|
}
|
|
fn code(k: &changes::Kind) -> char {
|
|
match k {
|
|
changes::Kind::Added => 'A',
|
|
changes::Kind::Modified => 'M',
|
|
changes::Kind::Deleted => 'D',
|
|
changes::Kind::Renamed => 'R',
|
|
}
|
|
}
|