userland/git: git-compatible CLI on v2's gix code (T10)
Local workflow, smart-HTTP clone/fetch/push and public HTTPS clone/fetch match native git in stdout and exit codes. Only WASI imports (wasi:http, exit-with-code).
This commit is contained in:
@@ -0,0 +1,827 @@
|
||||
//! Local Git operations adapted from the v2 gix plugin, without RPC or host imports.
|
||||
#![allow(dead_code)]
|
||||
mod branches;
|
||||
mod changes;
|
||||
mod checkout;
|
||||
mod http;
|
||||
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())),
|
||||
})
|
||||
}
|
||||
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>,
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
Show {
|
||||
revision: Option<String>,
|
||||
},
|
||||
Clone {
|
||||
url: String,
|
||||
path: Option<String>,
|
||||
},
|
||||
Fetch {
|
||||
#[arg(default_value = "origin")]
|
||||
remote: 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,
|
||||
key: String,
|
||||
value: Option<String>,
|
||||
},
|
||||
RevParse {
|
||||
#[arg(long)]
|
||||
show_toplevel: bool,
|
||||
#[arg(long)]
|
||||
git_dir: bool,
|
||||
#[arg(long)]
|
||||
abbrev_ref: bool,
|
||||
revision: Option<String>,
|
||||
},
|
||||
Status {
|
||||
#[arg(short, long)]
|
||||
short: bool,
|
||||
#[arg(long)]
|
||||
porcelain: bool,
|
||||
},
|
||||
Add {
|
||||
#[arg(short = 'A', long)]
|
||||
all: bool,
|
||||
paths: Vec<String>,
|
||||
},
|
||||
Rm {
|
||||
#[arg(long)]
|
||||
cached: bool,
|
||||
paths: Vec<String>,
|
||||
},
|
||||
Commit {
|
||||
#[arg(short = 'm')]
|
||||
message: String,
|
||||
#[arg(short, long)]
|
||||
all: bool,
|
||||
#[arg(long)]
|
||||
allow_empty: bool,
|
||||
},
|
||||
Log {
|
||||
#[arg(long)]
|
||||
oneline: bool,
|
||||
#[arg(short = 'n', default_value = "50")]
|
||||
limit: usize,
|
||||
revision: Option<String>,
|
||||
},
|
||||
Diff {
|
||||
#[arg(long, alias = "staged")]
|
||||
cached: bool,
|
||||
#[arg(long)]
|
||||
stat: bool,
|
||||
},
|
||||
Branch {
|
||||
#[arg(short = 'd')]
|
||||
delete: bool,
|
||||
#[arg(short = 'D')]
|
||||
force_delete: bool,
|
||||
name: Option<String>,
|
||||
start: Option<String>,
|
||||
},
|
||||
Switch {
|
||||
#[arg(short = 'c')]
|
||||
create: bool,
|
||||
target: String,
|
||||
},
|
||||
Checkout {
|
||||
#[arg(short = 'b')]
|
||||
create: bool,
|
||||
target: String,
|
||||
},
|
||||
Merge {
|
||||
#[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,
|
||||
},
|
||||
}
|
||||
#[derive(Subcommand)]
|
||||
enum Worktree {
|
||||
List {
|
||||
#[arg(long)]
|
||||
porcelain: bool,
|
||||
},
|
||||
Add {
|
||||
#[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> {
|
||||
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 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))?;
|
||||
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 {
|
||||
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::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 {
|
||||
show_toplevel,
|
||||
git_dir,
|
||||
abbrev_ref,
|
||||
revision,
|
||||
} => {
|
||||
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 {
|
||||
println!(
|
||||
"{}",
|
||||
repository
|
||||
.rev_parse_single(required(revision, "revision")?.as_str())
|
||||
.map_err(|e| error("revision", e))?
|
||||
);
|
||||
}
|
||||
}
|
||||
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,
|
||||
all,
|
||||
allow_empty,
|
||||
} => {
|
||||
let before = head_info(&repository)?;
|
||||
if !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: None,
|
||||
allow_empty: Some(allow_empty),
|
||||
},
|
||||
)?;
|
||||
println!(
|
||||
"[{}{} {}] {}",
|
||||
before
|
||||
.branch
|
||||
.unwrap_or_else(|| "HEAD".into())
|
||||
.trim_start_matches("refs/heads/"),
|
||||
if before.unborn { " (root-commit)" } else { "" },
|
||||
&result.id[..7],
|
||||
message
|
||||
);
|
||||
let old = match 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 { short, porcelain } => {
|
||||
let mut status = changes::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 {
|
||||
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 {
|
||||
output::long_status(&status);
|
||||
return Ok(0);
|
||||
}
|
||||
let mut rows = std::collections::BTreeMap::new();
|
||||
for c in status.staged {
|
||||
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 (p, c) in rows {
|
||||
println!("{}{} {p}", c[0], c[1]);
|
||||
}
|
||||
}
|
||||
Command::Diff { cached, stat } => {
|
||||
let diffs = changes::diff(changes::DiffParams {
|
||||
repo: path,
|
||||
from: None,
|
||||
to: None,
|
||||
staged: Some(cached),
|
||||
path: None,
|
||||
context: None,
|
||||
})?;
|
||||
if stat {
|
||||
output::stat(&diffs, false);
|
||||
} else {
|
||||
for diff in diffs {
|
||||
print!("{}", diff.patch);
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::Log {
|
||||
oneline,
|
||||
limit,
|
||||
revision,
|
||||
} => {
|
||||
let commit = repository
|
||||
.rev_parse_single(revision.as_deref().unwrap_or("HEAD"))
|
||||
.map_err(|e| error("log", e))?
|
||||
.object()
|
||||
.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
|
||||
.map_err(|e| error("log", e))?
|
||||
.object()
|
||||
.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 {
|
||||
println!(
|
||||
"{} {}",
|
||||
&c.id.to_string()[..7],
|
||||
message.lines().next().unwrap_or("")
|
||||
);
|
||||
} else {
|
||||
if !first {
|
||||
println!();
|
||||
}
|
||||
output::commit(&c)?;
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::Branch {
|
||||
delete,
|
||||
force_delete,
|
||||
name,
|
||||
start,
|
||||
} => {
|
||||
if let Some(name) = name {
|
||||
if delete || force_delete {
|
||||
branches::delete(
|
||||
&repository,
|
||||
branches::BranchDeleteParams {
|
||||
repo: path,
|
||||
name,
|
||||
force: Some(force_delete),
|
||||
},
|
||||
)?;
|
||||
} else {
|
||||
branches::create(
|
||||
&repository,
|
||||
branches::BranchCreateParams {
|
||||
repo: path,
|
||||
name,
|
||||
from: start,
|
||||
checkout: Some(false),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
let head = head_info(&repository)?;
|
||||
for branch in repository
|
||||
.references()
|
||||
.map_err(|e| error("branch", e))?
|
||||
.local_branches()
|
||||
.map_err(|e| error("branch", e))?
|
||||
{
|
||||
let branch = branch.map_err(|e| error("branch", e))?;
|
||||
println!(
|
||||
"{} {}",
|
||||
if head.branch.as_deref()
|
||||
== Some(branch.name().as_bstr().to_str_lossy().as_ref())
|
||||
{
|
||||
"*"
|
||||
} else {
|
||||
" "
|
||||
},
|
||||
branch.name().shorten()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::Switch { create, target } | Command::Checkout { create, target } => {
|
||||
branches::checkout(
|
||||
&repository,
|
||||
branches::CheckoutParams {
|
||||
repo: path,
|
||||
target,
|
||||
create: Some(create),
|
||||
force: None,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
Command::Merge {
|
||||
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,
|
||||
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 {
|
||||
println!("Already up to date.");
|
||||
}
|
||||
output::conflicts(&r.conflicts, true);
|
||||
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 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::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 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 {
|
||||
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,
|
||||
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::Show { revision } => {
|
||||
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))?;
|
||||
output::commit(&commit)?;
|
||||
println!();
|
||||
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(),
|
||||
};
|
||||
for diff in changes::diff(changes::DiffParams {
|
||||
repo: path,
|
||||
from: Some(old),
|
||||
to: Some(commit.id.to_string()),
|
||||
staged: None,
|
||||
path: None,
|
||||
context: None,
|
||||
})? {
|
||||
print!("{}", diff.patch);
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::Fetch { remote } => network::fetch(&path, &remote)?,
|
||||
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',
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user