git: match native agent workflows
This commit is contained in:
+587
-64
@@ -55,18 +55,28 @@ fn identity(name: Option<String>, email: Option<String>) -> Result<Identity> {
|
||||
.or_else(|| config.string("user.email").map(|s| s.to_string())),
|
||||
})
|
||||
}
|
||||
mod cherry_pick;
|
||||
mod history;
|
||||
mod local;
|
||||
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 {
|
||||
Show {
|
||||
#[arg(long)]
|
||||
stat: bool,
|
||||
revision: Option<String>,
|
||||
},
|
||||
Clone {
|
||||
@@ -96,6 +106,8 @@ enum Command {
|
||||
Config {
|
||||
#[arg(long)]
|
||||
local: bool,
|
||||
#[arg(long)]
|
||||
get: bool,
|
||||
key: String,
|
||||
value: Option<String>,
|
||||
},
|
||||
@@ -106,13 +118,17 @@ enum Command {
|
||||
git_dir: bool,
|
||||
#[arg(long)]
|
||||
abbrev_ref: bool,
|
||||
revision: Option<String>,
|
||||
#[arg(long)]
|
||||
short: bool,
|
||||
revisions: Vec<String>,
|
||||
},
|
||||
Status {
|
||||
#[arg(short, long)]
|
||||
short: bool,
|
||||
#[arg(long)]
|
||||
porcelain: 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)]
|
||||
@@ -126,7 +142,13 @@ enum Command {
|
||||
},
|
||||
Commit {
|
||||
#[arg(short = 'm')]
|
||||
message: String,
|
||||
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)]
|
||||
@@ -140,19 +162,42 @@ enum Command {
|
||||
oneline: bool,
|
||||
#[arg(short = 'n', default_value = "50")]
|
||||
limit: usize,
|
||||
#[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, 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)]
|
||||
show_current: bool,
|
||||
#[arg(short = 'a')]
|
||||
all: bool,
|
||||
#[arg(short = 'd')]
|
||||
delete: bool,
|
||||
#[arg(short = 'D')]
|
||||
force_delete: bool,
|
||||
#[arg(short = 'm')]
|
||||
rename: bool,
|
||||
name: Option<String>,
|
||||
start: Option<String>,
|
||||
},
|
||||
@@ -164,7 +209,8 @@ enum Command {
|
||||
Checkout {
|
||||
#[arg(short = 'b')]
|
||||
create: bool,
|
||||
target: String,
|
||||
target: Option<String>,
|
||||
paths: Vec<String>,
|
||||
},
|
||||
Merge {
|
||||
#[arg(long)]
|
||||
@@ -192,6 +238,54 @@ enum Command {
|
||||
#[command(subcommand)]
|
||||
command: Worktree,
|
||||
},
|
||||
LsFiles,
|
||||
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 {
|
||||
@@ -215,6 +309,24 @@ 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))?;
|
||||
}
|
||||
@@ -225,24 +337,39 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
} = cli.command
|
||||
{
|
||||
let path = path.unwrap_or_else(|| ".".into());
|
||||
let repo = if bare {
|
||||
gix::init_bare(&path)
|
||||
let existing = std::path::Path::new(&path)
|
||||
.join(if bare { "HEAD" } else { ".git/HEAD" })
|
||||
.exists();
|
||||
let repo = if existing {
|
||||
repo::open(&path)
|
||||
} else {
|
||||
gix::init(&path)
|
||||
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()
|
||||
);
|
||||
}
|
||||
.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))?;
|
||||
println!(
|
||||
"Initialized empty Git repository in {}/",
|
||||
repo.git_dir()
|
||||
.canonicalize()
|
||||
.map_err(|e| error("init", e))?
|
||||
.display()
|
||||
);
|
||||
return Ok(0);
|
||||
}
|
||||
if let Command::Clone { url, path } = cli.command {
|
||||
@@ -266,6 +393,101 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
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::LsFiles => {
|
||||
let index = repository
|
||||
.index_or_empty()
|
||||
.map_err(|e| error("ls-files", e))?;
|
||||
for entry in index.entries() {
|
||||
println!("{}", entry.path(&index));
|
||||
}
|
||||
}
|
||||
Command::Config { key, value, .. } => {
|
||||
let file = repository.common_dir().join("config");
|
||||
if let Some(value) = value {
|
||||
@@ -291,7 +513,8 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
show_toplevel,
|
||||
git_dir,
|
||||
abbrev_ref,
|
||||
revision,
|
||||
short,
|
||||
revisions,
|
||||
} => {
|
||||
if show_toplevel {
|
||||
println!("{}", root.display());
|
||||
@@ -306,12 +529,13 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
.trim_start_matches("refs/heads/")
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
repository
|
||||
.rev_parse_single(required(revision, "revision")?.as_str())
|
||||
for revision in revisions {
|
||||
let id = repository
|
||||
.rev_parse_single(revision.as_str())
|
||||
.map_err(|e| error("revision", e))?
|
||||
);
|
||||
.to_string();
|
||||
println!("{}", if short { &id[..7] } else { &id });
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::Add { all, mut paths } => {
|
||||
@@ -341,12 +565,33 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
}
|
||||
Command::Commit {
|
||||
message,
|
||||
file,
|
||||
amend,
|
||||
no_edit,
|
||||
all,
|
||||
allow_empty,
|
||||
..
|
||||
} => {
|
||||
let before = head_info(&repository)?;
|
||||
if !allow_empty && merge::pending_parent(&repository)?.is_none() {
|
||||
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 = message.trim_end().to_owned();
|
||||
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())
|
||||
@@ -364,10 +609,13 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
author_name: None,
|
||||
author_email: None,
|
||||
all: Some(all),
|
||||
amend: None,
|
||||
amend: Some(amend),
|
||||
allow_empty: Some(allow_empty),
|
||||
},
|
||||
)?;
|
||||
if quiet {
|
||||
return Ok(0);
|
||||
}
|
||||
println!(
|
||||
"[{}{} {}] {}",
|
||||
before
|
||||
@@ -376,9 +624,31 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
.trim_start_matches("refs/heads/"),
|
||||
if before.unborn { " (root-commit)" } else { "" },
|
||||
&result.id[..7],
|
||||
message
|
||||
message.lines().next().unwrap_or("")
|
||||
);
|
||||
let old = match before.id {
|
||||
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![] })
|
||||
@@ -394,7 +664,11 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
context: None,
|
||||
})?);
|
||||
}
|
||||
Command::Status { short, porcelain } => {
|
||||
Command::Status {
|
||||
short,
|
||||
porcelain,
|
||||
branch,
|
||||
} => {
|
||||
let mut status = changes::status(&repository)?;
|
||||
let index = repository
|
||||
.index_or_empty()
|
||||
@@ -414,10 +688,30 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
untracked.insert(collapsed.to_owned());
|
||||
}
|
||||
status.untracked = untracked.into_iter().collect();
|
||||
if !short && !porcelain {
|
||||
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();
|
||||
for c in status.staged {
|
||||
rows.entry(c.path).or_insert([' '; 2])[0] = code(&c.status);
|
||||
@@ -431,20 +725,45 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
for p in status.conflicted {
|
||||
rows.insert(p, ['U', 'U']);
|
||||
}
|
||||
for (p, c) in rows {
|
||||
println!("{}{} {p}", c[0], c[1]);
|
||||
for untracked in [false, true] {
|
||||
for (p, c) in &rows {
|
||||
if (c == &['?', '?']) == untracked {
|
||||
println!("{}{} {p}", c[0], c[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::Diff { cached, stat } => {
|
||||
Command::Diff {
|
||||
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())
|
||||
};
|
||||
let diffs = changes::diff(changes::DiffParams {
|
||||
repo: path,
|
||||
from: None,
|
||||
to: None,
|
||||
from,
|
||||
to,
|
||||
staged: Some(cached),
|
||||
path: None,
|
||||
path: paths.first().cloned(),
|
||||
context: None,
|
||||
})?;
|
||||
if stat {
|
||||
if name_only || name_status {
|
||||
for diff in diffs {
|
||||
if name_status {
|
||||
println!("{}\t{}", code(&diff.status), diff.path);
|
||||
} else {
|
||||
println!("{}", diff.path);
|
||||
}
|
||||
}
|
||||
} else if stat {
|
||||
output::stat(&diffs, false);
|
||||
} else {
|
||||
for diff in diffs {
|
||||
@@ -455,7 +774,12 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
Command::Log {
|
||||
oneline,
|
||||
limit,
|
||||
format,
|
||||
pretty,
|
||||
patch,
|
||||
stat,
|
||||
revision,
|
||||
paths,
|
||||
} => {
|
||||
let commit = repository
|
||||
.rev_parse_single(revision.as_deref().unwrap_or("HEAD"))
|
||||
@@ -464,24 +788,102 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
.map_err(|e| error("log", e))?
|
||||
.peel_to_commit()
|
||||
.map_err(|e| error("log", e))?;
|
||||
let mut first = true;
|
||||
for info in commit
|
||||
.ancestors()
|
||||
.use_commit_graph(false)
|
||||
.sorting(gix::revision::walk::Sorting::ByCommitTime(
|
||||
Default::default(),
|
||||
))
|
||||
.all()
|
||||
.map_err(|e| error("log", e))?
|
||||
.take(limit)
|
||||
{
|
||||
let c = info
|
||||
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))?
|
||||
.object()
|
||||
.map_err(|e| error("log", e))?;
|
||||
.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().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();
|
||||
if oneline {
|
||||
let diffs = if patch || stat || !paths.is_empty() {
|
||||
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(changes::DiffParams {
|
||||
repo: path.clone(),
|
||||
from: Some(old),
|
||||
to: Some(c.id.to_string()),
|
||||
staged: None,
|
||||
path: paths.first().cloned(),
|
||||
context: None,
|
||||
})?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
if let Some(template) = format.as_deref().or(pretty
|
||||
.as_deref()
|
||||
.map(|s| s.strip_prefix("format:").unwrap_or(s)))
|
||||
{
|
||||
let author = c.author().map_err(|e| error("log author", e))?;
|
||||
let date = author
|
||||
.time()
|
||||
.map_err(|e| error("log date", e))?
|
||||
.format_or_unix(gix::date::time::format::DEFAULT);
|
||||
let mut labels = Vec::new();
|
||||
for reference in repository
|
||||
.references()
|
||||
.map_err(|e| error("log refs", e))?
|
||||
.all()
|
||||
.map_err(|e| error("log refs", e))?
|
||||
{
|
||||
let reference = reference.map_err(|e| error("log refs", e))?;
|
||||
let name = reference.name().as_bstr().to_str_lossy();
|
||||
if repository
|
||||
.rev_parse_single(name.as_ref())
|
||||
.map(|id| id.to_string() == c.id.to_string())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let short = reference.name().shorten().to_string();
|
||||
labels.push(
|
||||
if head_info(&repository)?.branch.as_deref() == Some(name.as_ref())
|
||||
{
|
||||
format!("HEAD -> {short}")
|
||||
} else {
|
||||
short
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
let decorations = if labels.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" ({})", labels.join(", "))
|
||||
};
|
||||
let text = template
|
||||
.replace("%H", &c.id.to_string())
|
||||
.replace("%h", &c.id.to_string()[..7])
|
||||
.replace("%s", message.lines().next().unwrap_or(""))
|
||||
.replace("%an", &author.name.to_string())
|
||||
.replace("%ae", &author.email.to_string())
|
||||
.replace("%ad", &date)
|
||||
.replace("%d", &decorations);
|
||||
if pretty.is_some() {
|
||||
if !first {
|
||||
println!();
|
||||
}
|
||||
print!("{text}");
|
||||
} else {
|
||||
println!("{text}");
|
||||
}
|
||||
first = false;
|
||||
} else if oneline {
|
||||
println!(
|
||||
"{} {}",
|
||||
&c.id.to_string()[..7],
|
||||
@@ -494,24 +896,61 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
output::commit(&c)?;
|
||||
first = false;
|
||||
}
|
||||
if patch || stat {
|
||||
println!();
|
||||
if stat {
|
||||
output::stat(&diffs, false);
|
||||
}
|
||||
if patch {
|
||||
for diff in diffs {
|
||||
print!("{}", diff.patch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::Branch {
|
||||
delete,
|
||||
force_delete,
|
||||
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 {
|
||||
if delete || force_delete {
|
||||
let id = branches::resolve(&repository, &name)?.to_string();
|
||||
branches::delete(
|
||||
&repository,
|
||||
branches::BranchDeleteParams {
|
||||
repo: path,
|
||||
name,
|
||||
name: name.clone(),
|
||||
force: Some(force_delete),
|
||||
},
|
||||
)?;
|
||||
if !quiet {
|
||||
println!("Deleted branch {name} (was {}).", &id[..7]);
|
||||
}
|
||||
} else {
|
||||
branches::create(
|
||||
&repository,
|
||||
@@ -528,10 +967,16 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
for branch in repository
|
||||
.references()
|
||||
.map_err(|e| error("branch", e))?
|
||||
.local_branches()
|
||||
.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;
|
||||
}
|
||||
println!(
|
||||
"{} {}",
|
||||
if head.branch.as_deref()
|
||||
@@ -541,12 +986,48 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
} else {
|
||||
" "
|
||||
},
|
||||
branch.name().shorten()
|
||||
if name.starts_with("refs/remotes/") {
|
||||
format!("remotes/{}", name.trim_start_matches("refs/remotes/"))
|
||||
} else {
|
||||
branch.name().shorten().to_string()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::Switch { create, target } | Command::Checkout { create, target } => {
|
||||
Command::Checkout {
|
||||
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: required(target, "checkout target")?,
|
||||
create: Some(create),
|
||||
force: None,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Command::Switch { create, target } => {
|
||||
branches::checkout(
|
||||
&repository,
|
||||
branches::CheckoutParams {
|
||||
@@ -588,10 +1069,13 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
},
|
||||
)?
|
||||
};
|
||||
if r.already_up_to_date {
|
||||
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);
|
||||
}
|
||||
@@ -637,6 +1121,10 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
},
|
||||
)?
|
||||
};
|
||||
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(
|
||||
@@ -766,7 +1254,7 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
)?;
|
||||
}
|
||||
},
|
||||
Command::Show { revision } => {
|
||||
Command::Show { revision, stat } => {
|
||||
let object = repository
|
||||
.rev_parse_single(revision.as_deref().unwrap_or("HEAD"))
|
||||
.map_err(|e| error("show", e))?
|
||||
@@ -779,8 +1267,38 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
.map_err(|e| error("show", e))?;
|
||||
} else {
|
||||
let commit = object.peel_to_commit().map_err(|e| error("show", e))?;
|
||||
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);
|
||||
}
|
||||
}
|
||||
output::commit(&commit)?;
|
||||
println!();
|
||||
if merge_commit && !stat {
|
||||
return Ok(0);
|
||||
}
|
||||
let old = match commit.parent_ids().next() {
|
||||
Some(id) => id.to_string(),
|
||||
None => repository
|
||||
@@ -788,15 +1306,20 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
.map_err(|e| error("show", e))?
|
||||
.to_string(),
|
||||
};
|
||||
for diff in changes::diff(changes::DiffParams {
|
||||
let diffs = changes::diff(changes::DiffParams {
|
||||
repo: path,
|
||||
from: Some(old),
|
||||
to: Some(commit.id.to_string()),
|
||||
staged: None,
|
||||
path: None,
|
||||
context: None,
|
||||
})? {
|
||||
print!("{}", diff.patch);
|
||||
})?;
|
||||
if stat {
|
||||
output::stat(&diffs, false);
|
||||
} else {
|
||||
for diff in diffs {
|
||||
print!("{}", diff.patch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user