//! 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 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 = std::result::Result; #[derive(serde::Serialize, serde::Deserialize)] pub struct HeadInfo { detached: bool, unborn: bool, branch: Option, id: Option, } include!("head.inc"); struct Identity { name: Option, email: Option, } fn identity(name: Option, email: Option) -> Result { 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 plumbing; 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, #[arg(short = 'c', global = true)] config: Vec, #[arg(short = 'q', long, global = true)] quiet: bool, #[command(subcommand)] command: Command, } #[derive(Subcommand)] enum Command { Show { #[arg(long)] stat: bool, revision: Option, }, Clone { url: String, path: Option, }, Fetch { #[arg(default_value = "origin")] remote: String, revision: Option, }, MergeBase { #[arg(long)] is_ancestor: bool, a: String, b: String, }, RevList { #[arg(long)] count: bool, revisions: Vec, }, SymbolicRef { #[arg(long)] short: bool, name: String, }, UpdateRef { name: String, new: String, old: Option, }, Clean { #[arg(short = 'f')] force: bool, #[arg(short = 'd')] directories: bool, }, CheckIgnore { #[arg(long)] no_index: bool, paths: Vec, }, Push { #[arg(short = 'u', long)] set_upstream: bool, #[arg(short = 'f', long)] force: bool, #[arg(default_value = "origin")] remote: String, refspec: Option, }, Init { #[arg(long)] bare: bool, #[arg(short = 'b', default_value = "main")] initial_branch: String, path: Option, }, Config { #[arg(long)] local: bool, #[arg(long)] get: bool, key: String, value: Option, }, 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, }, 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, #[arg(short = 'b', long)] branch: bool, }, Add { #[arg(short = 'A', long)] all: bool, paths: Vec, }, Rm { #[arg(long)] cached: bool, paths: Vec, }, Commit { #[arg(short = 'm')] message: Option, #[arg(short = 'F', conflicts_with = "message")] file: Option, #[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)] name_only: bool, #[arg(long)] no_renames: bool, #[arg(long)] diff_merges: Option, #[arg(long)] oneline: bool, #[arg(short = 'n')] limit: Option, #[arg(long)] format: Option, #[arg(long)] pretty: Option, #[arg(short = 'p')] patch: bool, #[arg(long)] stat: bool, revision: Option, #[arg(last = true)] paths: Vec, }, Diff { #[arg(long)] numstat: bool, #[arg(long)] no_renames: bool, #[arg(long)] diff_filter: Option, #[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, #[arg(last = true)] paths: Vec, }, 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 = 'm')] rename: bool, name: Option, start: Option, }, 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, paths: Vec, }, 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, revision: Option, }, Rebase { #[arg(long)] abort: bool, #[arg(long = "continue")] resume: bool, #[arg(long)] skip: bool, upstream: Option, }, 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, paths: Vec, }, Restore { #[arg(long)] staged: bool, paths: Vec, }, Stash { #[arg(short = 'm', long)] message: Option, command: Option, entry: Option, }, Tag { #[arg(short = 'a')] annotated: bool, #[arg(short = 'm')] message: Option, #[arg(short = 'l', long)] list: bool, #[arg(short = 'd')] delete: bool, name: Option, revision: Option, }, Remote { #[arg(short = 'v', long)] verbose: bool, command: Option, name: Option, url: Option, }, CherryPick { #[arg(long = "continue")] resume: bool, #[arg(long)] abort: bool, revision: Option, }, } #[derive(Subcommand)] enum Worktree { Prune, List { #[arg(long)] porcelain: bool, }, Add { #[arg(long)] detach: bool, #[arg(short = 'b')] branch: Option, path: String, revision: Option, }, Remove { #[arg(long)] force: bool, path: String, }, } fn required(value: Option, what: &str) -> Result { value.ok_or_else(|| error(what, "argument required")) } pub fn run(cli: Cli) -> Result { let quiet = cli.quiet; let count = std::env::var("GIT_CONFIG_COUNT") .ok() .and_then(|s| s.parse::().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 | -F | --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 { 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, staged: Some(cached), path: paths.first().cloned(), context: None, }, !no_renames, )?; 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 { name_only, no_renames, diff_merges, oneline, limit, format, pretty, patch, stat, revision, paths, } => { let limit = 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::>>()? }; 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 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 = plumbing::format(template, &message) .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], message.lines().next().unwrap_or("") ); } else { if !first { println!(); } 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, 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 { 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::Show { revision, stat } => { 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))?; let merge_commit = commit.parent_ids().count() > 1; if merge_commit && !stat { let mut common: Option> = 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::>(); 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 .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 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', } }