From 397de2678e337019c198cf92323f0ee8a681534a Mon Sep 17 00:00:00 2001 From: altsem Date: Sun, 25 Feb 2024 15:03:43 +0100 Subject: [PATCH] refactor: use git2 for branch status --- Cargo.lock | 118 ---------- Cargo.toml | 2 - src/git/mod.rs | 23 +- src/git/parse/mod.rs | 1 - src/git/parse/status/mod.rs | 214 ------------------ src/git/parse/status/status.pest | 15 -- src/git/status.rs | 19 -- src/screen/status.rs | 96 ++++---- .../gitu__tests__commit_from_empty.snap | 2 +- src/snapshots/gitu__tests__moved_file.snap | 2 +- .../gitu__tests__unstaged_changes.snap | 2 +- 11 files changed, 60 insertions(+), 434 deletions(-) delete mode 100644 src/git/parse/mod.rs delete mode 100644 src/git/parse/status/mod.rs delete mode 100644 src/git/parse/status/status.pest diff --git a/Cargo.lock b/Cargo.lock index 31515bd16e..6248911660 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,15 +164,6 @@ version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "bstr" version = "0.2.17" @@ -359,15 +350,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "cpufeatures" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" -dependencies = [ - "libc", -] - [[package]] name = "criterion" version = "0.5.1" @@ -460,16 +442,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "debugid" version = "0.8.0" @@ -485,16 +457,6 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - [[package]] name = "dirs" version = "5.0.1" @@ -571,16 +533,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.2.12" @@ -626,8 +578,6 @@ dependencies = [ "insta", "itertools 0.12.1", "lazy_static", - "pest", - "pest_derive", "pprof", "pretty_assertions", "ratatui", @@ -1037,51 +987,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pest" -version = "2.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c0dcc30b6a27553f9cc242972b67f75b60eb0db71f0b5462f38b058c41546" -dependencies = [ - "memchr", - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e1288dbd7786462961e69bfd4df7848c1e37e8b74303dbdab82c3a9cdd2809" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1381c29a877c6d34b8c176e734f35d7f7f5b3adaefe940cb4d1bb7af94678e2e" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.50", -] - -[[package]] -name = "pest_meta" -version = "2.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0934d6907f148c22a3acbda520c7eed243ad7487a30f51f6ce52b58b7077a8a" -dependencies = [ - "once_cell", - "pest", - "sha2", -] - [[package]] name = "pkg-config" version = "0.3.30" @@ -1356,17 +1261,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "signal-hook" version = "0.3.17" @@ -1587,18 +1481,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "ucd-trie" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" - [[package]] name = "unicode-bidi" version = "0.3.15" diff --git a/Cargo.toml b/Cargo.toml index ca7a7f96a4..6524c01a51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,5 @@ git2 = "0.18.2" insta = "1.35.1" itertools = "0.12.0" lazy_static = "1.4.0" -pest = "2.7.6" -pest_derive = "2.7.6" ratatui = "0.26.1" similar = { version = "2.4.0", features = ["unicode"] } diff --git a/src/git/mod.rs b/src/git/mod.rs index d1f9356349..d1b3880337 100644 --- a/src/git/mod.rs +++ b/src/git/mod.rs @@ -9,21 +9,18 @@ use self::{ }; use crate::{git2_opts, Res}; use std::{ - error::Error, fs, io::ErrorKind, path::Path, process::Command, - str::{self, FromStr}, + str::{self}, }; pub(crate) mod commit; pub(crate) mod diff; pub(crate) mod merge_status; -mod parse; pub(crate) mod rebase_status; pub(crate) mod remote; -pub(crate) mod status; // TODO Check for.git/index.lock and block if it exists // TODO Use only plumbing commands @@ -176,10 +173,6 @@ pub(crate) fn diff_staged(repo: &Repository) -> Res { convert_diff(diff) } -pub(crate) fn status(dir: &Path) -> Res { - run_git(dir, &["status", "--porcelain", "--branch"], &[]) -} - pub(crate) fn show(repo: &Repository, reference: &str) -> Res { let object = &repo.revparse_single(reference)?; @@ -318,20 +311,6 @@ pub(crate) fn checkout_ref_cmd(reference: &str) -> Command { git(&["checkout", reference]) } -fn run_git>>( - dir: &Path, - args: &[&str], - meta_args: &[&str], -) -> Res { - let out = Command::new("git") - .args(&[args, meta_args].concat()) - .current_dir(dir) - .output()? - .stdout; - - str::from_utf8(&out)?.parse() -} - fn git(args: &[&str]) -> Command { let mut cmd = Command::new("git"); cmd.args(args); diff --git a/src/git/parse/mod.rs b/src/git/parse/mod.rs deleted file mode 100644 index 1df1d86788..0000000000 --- a/src/git/parse/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(super) mod status; diff --git a/src/git/parse/status/mod.rs b/src/git/parse/status/mod.rs deleted file mode 100644 index 3245cbcbe9..0000000000 --- a/src/git/parse/status/mod.rs +++ /dev/null @@ -1,214 +0,0 @@ -use std::{error::Error, str::FromStr}; - -use pest::Parser; -use pest_derive::Parser; - -use crate::git::status::{BranchStatus, Status, StatusFile}; - -// TODO Get rid of this, use libgit2 instead -#[derive(Parser)] -#[grammar = "git/parse/status/status.pest"] // relative to src -struct StatusParser; - -impl FromStr for Status { - type Err = Box; - - fn from_str(s: &str) -> Result { - let mut local = None; - let mut remote = None; - let mut ahead = 0; - let mut behind = 0; - let mut files = vec![]; - - for line in StatusParser::parse(Rule::status_lines, s)? { - match line.as_rule() { - Rule::no_repo => (), - Rule::branch_status => { - for pair in line.into_inner() { - match pair.as_rule() { - Rule::no_commits => (), - Rule::no_branch => (), - Rule::local => local = Some(pair.as_str().to_string()), - Rule::remote => remote = Some(pair.as_str().to_string()), - Rule::ahead => ahead = pair.as_str().parse().unwrap(), - Rule::behind => behind = pair.as_str().parse().unwrap(), - rule => panic!("No rule {:?}", rule), - } - } - } - Rule::file_status => { - let mut status_code = None; - let mut path = None; - let mut new_path = None; - - for pair in line.into_inner() { - match pair.as_rule() { - Rule::code => { - let mut chars = pair.as_str().chars(); - status_code = Some([chars.next().unwrap(), chars.next().unwrap()]); - } - Rule::file => path = Some(pair.as_str().to_string()), - Rule::new_file => new_path = Some(pair.as_str().to_string()), - rule => panic!("No rule {:?}", rule), - } - } - - files.push(StatusFile { - status_code: status_code.expect("Error parsing status_code"), - path: path.expect("Error parsing path"), - new_path, - }); - } - _ => panic!("No rule {:?}", line.as_rule()), - } - } - - Ok(Status { - branch_status: BranchStatus { - local, - remote, - ahead, - behind, - }, - files, - }) - } -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use crate::git::{status::BranchStatus, status::Status, status::StatusFile}; - - #[test] - fn parse_simple() { - let input = "## master...origin/master\n M src/git.rs\n R foo -> bar\n?? spaghet\n"; - - assert_eq!( - Status::from_str(input).unwrap(), - Status { - branch_status: BranchStatus { - local: Some("master".to_string()), - remote: Some("origin/master".to_string()), - ahead: 0, - behind: 0 - }, - files: vec![ - StatusFile { - status_code: [' ', 'M'], - path: "src/git.rs".to_string(), - new_path: None, - }, - StatusFile { - status_code: [' ', 'R'], - path: "foo".to_string(), - new_path: Some("bar".to_string()) - }, - StatusFile { - status_code: ['?', '?'], - path: "spaghet".to_string(), - new_path: None, - }, - ] - } - ); - } - - #[test] - fn parse_ahead() { - let input = "## master...origin/master [ahead 1]\n"; - - assert_eq!( - Status::from_str(input).unwrap(), - Status { - branch_status: BranchStatus { - local: Some("master".to_string()), - remote: Some("origin/master".to_string()), - ahead: 1, - behind: 0 - }, - files: vec![] - } - ); - } - - #[test] - fn parse_behind() { - let input = "## master...origin/master [behind 1]\n"; - - assert_eq!( - Status::from_str(input).unwrap(), - Status { - branch_status: BranchStatus { - local: Some("master".to_string()), - remote: Some("origin/master".to_string()), - ahead: 0, - behind: 1 - }, - files: vec![] - } - ); - } - - #[test] - fn parse_diverge() { - let input = "## master...origin/master [ahead 1, behind 1]\n"; - - assert_eq!( - Status::from_str(input).unwrap(), - Status { - branch_status: BranchStatus { - local: Some("master".to_string()), - remote: Some("origin/master".to_string()), - ahead: 1, - behind: 1 - }, - files: vec![] - } - ); - } - - #[test] - fn parse_no_remote() { - let input = "## test.lol\n"; - - assert_eq!( - Status::from_str(input).unwrap(), - Status { - branch_status: BranchStatus { - local: Some("test.lol".to_string()), - remote: None, - ahead: 0, - behind: 0 - }, - files: vec![] - } - ); - } - - #[test] - fn messy_file_name() { - let input = r#"## master...origin/master -?? "spaghet lol.testing !@#$%^&*()" -?? src/diff.pest -"#; - assert_eq!(Status::from_str(input).unwrap().files.len(), 2); - } - - #[test] - fn no_branch() { - assert_eq!( - Status::from_str("## HEAD (no branch)\n").unwrap(), - Status { - branch_status: BranchStatus { - local: None, - remote: None, - ahead: 0, - behind: 0 - }, - files: vec![] - } - ); - } -} diff --git a/src/git/parse/status/status.pest b/src/git/parse/status/status.pest deleted file mode 100644 index 3378503531..0000000000 --- a/src/git/parse/status/status.pest +++ /dev/null @@ -1,15 +0,0 @@ -branch = @{ !(".." | NEWLINE | " ") ~ ANY ~ branch | "" } -local = @{ branch } -remote = @{ branch } -ahead = { ASCII_DIGIT+ } -behind = { ASCII_DIGIT+ } -ahead_behind = _{ "[" ~ (("ahead " ~ ahead | "behind " ~ behind) ~ ", "?)+ ~ "]" } -no_commits = { "No commits yet on " ~ local } -no_branch = { "HEAD (no branch)" } -branch_status = { "## " ~ (no_commits | no_branch | local ~ ("..." ~ remote)? ~ (" " ~ ahead_behind | " [gone]")?) } -file = { (!(NEWLINE | " -> ") ~ ANY)+ } -new_file = @{ file } -code = { (ASCII_ALPHA | "?" | " "){2} } -file_status = { code ~ " " ~ file ~ (" -> " ~ new_file)? } -no_repo = { "" } -status_lines = _{ branch_status ~ NEWLINE ~ (file_status ~ NEWLINE)* | no_repo } diff --git a/src/git/status.rs b/src/git/status.rs index db8cc8857a..8b13789179 100644 --- a/src/git/status.rs +++ b/src/git/status.rs @@ -1,20 +1 @@ -#[derive(Debug, PartialEq, Eq)] -pub(crate) struct Status { - pub branch_status: BranchStatus, - pub files: Vec, -} -#[derive(Debug, PartialEq, Eq)] -pub(crate) struct BranchStatus { - pub local: Option, - pub remote: Option, - pub ahead: u32, - pub behind: u32, -} - -#[derive(Debug, PartialEq, Eq)] -pub(crate) struct StatusFile { - pub status_code: [char; 2], - pub path: String, - pub new_path: Option, -} diff --git a/src/screen/status.rs b/src/screen/status.rs index 3a11c20e5f..bcd3b5027f 100644 --- a/src/screen/status.rs +++ b/src/screen/status.rs @@ -1,8 +1,8 @@ -use std::{iter, rc::Rc}; +use std::rc::Rc; use super::Screen; use crate::{ - git::{self, diff::Diff, status::BranchStatus}, + git::{self, diff::Diff}, git2_opts, items::{self, Item}, theme::CURRENT_THEME, @@ -18,9 +18,6 @@ pub(crate) fn create(repo: Rc, config: &Config, size: Rect) -> Res, config: &Config, size: Rect) -> Res) -> Vec { .collect::>() } -fn branch_status_items(status: &BranchStatus) -> Vec { - match (&status.local, &status.remote) { - (None, None) => vec![Item { +fn branch_status_items(repo: &Repository) -> Res> { + let Ok(head) = repo.head() else { + return Ok(vec![Item { id: "branch_status".into(), display: Text::from("No branch".fg(CURRENT_THEME.section).bold()), section: true, depth: 0, ..Default::default() - }], - (Some(local), maybe_remote) => Vec::from_iter( - iter::once(Item { - id: "branch_status".into(), - display: Text::from( - format!("On branch {}", local) - .fg(CURRENT_THEME.section) - .bold(), - ), - section: true, - depth: 0, - ..Default::default() - }) - .chain( - maybe_remote - .as_ref() - .map(|remote| branch_status_remote_description(status, remote)), - ), + }]); + }; + + let mut items = vec![Item { + id: "branch_status".into(), + display: Text::from( + format!("On branch {}", head.shorthand().unwrap()) + .fg(CURRENT_THEME.section) + .bold(), ), - (None, Some(_)) => unreachable!(), - } -} + section: true, + depth: 0, + ..Default::default() + }]; -fn branch_status_remote_description(status: &BranchStatus, remote: &str) -> Item { - Item { + let Ok(upstream) = repo.branch_upstream_name(head.name().unwrap()) else { + return Ok(items); + }; + let upstream_name = upstream.as_str().unwrap().to_string(); + let upstream_shortname = upstream_name + .strip_prefix("refs/remotes/") + .unwrap_or(&upstream_name) + .to_string(); + + let Ok(upstream_id) = repo.refname_to_id(&upstream_name) else { + items.push(Item { + id: "branch_status".into(), + display: format!( + "Your branch is based on '{}', but the upstream is gone.", + upstream_shortname + ) + .into(), + depth: 1, + unselectable: true, + ..Default::default() + }); + return Ok(items); + }; + + let (ahead, behind) = repo.graph_ahead_behind(head.target().unwrap(), upstream_id)?; + + items.push(Item { id: "branch_status".into(), - display: if status.ahead == 0 && status.behind == 0 { - Text::raw(format!("Your branch is up to date with '{}'.", remote)) - } else if status.ahead > 0 && status.behind == 0 { + display: if ahead == 0 && behind == 0 { + Text::raw(format!("Your branch is up to date with '{}'.", upstream_shortname)) + } else if ahead > 0 && behind == 0 { Text::raw(format!( "Your branch is ahead of '{}' by {} commit.", - remote, status.ahead + upstream_shortname, ahead )) - } else if status.ahead == 0 && status.behind > 0 { + } else if ahead == 0 && behind > 0 { Text::raw(format!( "Your branch is behind '{}' by {} commit.", - remote, status.behind + upstream_shortname, behind )) } else { - Text::raw(format!("Your branch and '{}' have diverged,\nand have {} and {} different commits each, respectively.", remote, status.ahead, status.behind)) + Text::raw(format!("Your branch and '{}' have diverged,\nand have {} and {} different commits each, respectively.", upstream_shortname, ahead, behind)) }, depth: 1, unselectable: true, ..Default::default() - } + }); + + Ok(items) } fn create_status_section_items<'a>( diff --git a/src/snapshots/gitu__tests__commit_from_empty.snap b/src/snapshots/gitu__tests__commit_from_empty.snap index 879fd4791e..04d5b2cb77 100644 --- a/src/snapshots/gitu__tests__commit_from_empty.snap +++ b/src/snapshots/gitu__tests__commit_from_empty.snap @@ -6,7 +6,7 @@ Buffer { area: Rect { x: 0, y: 0, width: 60, height: 10 }, content: [ "🢒On branch main ", - " Your branch is up to date with 'origin/main'. ", + " Your branch is based on 'origin/main', but the upstream is…", " ", " Recent commits ", " _______ main add new-file ", diff --git a/src/snapshots/gitu__tests__moved_file.snap b/src/snapshots/gitu__tests__moved_file.snap index f120c5451c..7788ae911d 100644 --- a/src/snapshots/gitu__tests__moved_file.snap +++ b/src/snapshots/gitu__tests__moved_file.snap @@ -6,7 +6,7 @@ Buffer { area: Rect { x: 0, y: 0, width: 60, height: 20 }, content: [ "🢒On branch main ", - " Your branch is up to date with 'origin/main'. ", + " Your branch is based on 'origin/main', but the upstream is…", " ", " Staged changes (2) ", " moved-file ", diff --git a/src/snapshots/gitu__tests__unstaged_changes.snap b/src/snapshots/gitu__tests__unstaged_changes.snap index 88a4478482..5c46561b20 100644 --- a/src/snapshots/gitu__tests__unstaged_changes.snap +++ b/src/snapshots/gitu__tests__unstaged_changes.snap @@ -6,7 +6,7 @@ Buffer { area: Rect { x: 0, y: 0, width: 60, height: 20 }, content: [ "🢒On branch main ", - " Your branch is up to date with 'origin/main'. ", + " Your branch is based on 'origin/main', but the upstream is…", " ", " Unstaged changes (1) ", " testfile ",