diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35935f00326..93050917dbf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: - name: Setup dependencies (macos) if: startsWith(matrix.os, 'macos') run: - brew install tree openssl + brew install tree openssl gnu-sed - name: "cargo check default features" if: startsWith(matrix.os, 'windows') uses: actions-rs/cargo@v1 diff --git a/git-repository/src/lib.rs b/git-repository/src/lib.rs index 7303cd73e19..490225dad7b 100644 --- a/git-repository/src/lib.rs +++ b/git-repository/src/lib.rs @@ -192,7 +192,9 @@ pub enum Path { /// mod types; -pub use types::{Commit, DetachedObject, Head, Id, Object, Reference, Repository, Tag, ThreadSafeRepository, Tree}; +pub use types::{ + Commit, DetachedObject, Head, Id, Object, Reference, Repository, RepositoryState, Tag, ThreadSafeRepository, Tree, +}; pub mod commit; pub mod head; diff --git a/git-repository/src/repository/mod.rs b/git-repository/src/repository/mod.rs index 15dca027d23..633036b3605 100644 --- a/git-repository/src/repository/mod.rs +++ b/git-repository/src/repository/mod.rs @@ -136,6 +136,8 @@ mod location; mod snapshots; +mod state; + mod impls; mod cache; diff --git a/git-repository/src/repository/state.rs b/git-repository/src/repository/state.rs new file mode 100644 index 00000000000..1dee00f3b8d --- /dev/null +++ b/git-repository/src/repository/state.rs @@ -0,0 +1,42 @@ +use crate::RepositoryState; + +impl crate::Repository { + /// Returns the status of an in progress operation on a repository or [`None`] + /// if nothing is happening. + pub fn in_progress_operation(&self) -> Option { + let git_dir = self.path(); + + // This is modeled on the logic from wt_status_get_state in git's wt-status.c and + // ps1 from git-prompt.sh. + + if git_dir.join("rebase-apply/applying").is_file() { + Some(RepositoryState::ApplyMailbox) + } else if git_dir.join("rebase-apply/rebasing").is_file() { + Some(RepositoryState::Rebase) + } else if git_dir.join("rebase-apply").is_dir() { + Some(RepositoryState::ApplyMailboxRebase) + } else if git_dir.join("rebase-merge/interactive").is_file() { + Some(RepositoryState::RebaseInteractive) + } else if git_dir.join("rebase-merge").is_dir() { + Some(RepositoryState::Rebase) + } else if git_dir.join("CHERRY_PICK_HEAD").is_file() { + if git_dir.join("todo").is_file() { + Some(RepositoryState::CherryPickSequence) + } else { + Some(RepositoryState::CherryPick) + } + } else if git_dir.join("MERGE_HEAD").is_file() { + Some(RepositoryState::Merge) + } else if git_dir.join("BISECT_LOG").is_file() { + Some(RepositoryState::Bisect) + } else if git_dir.join("REVERT_HEAD").is_file() { + if git_dir.join("todo").is_file() { + Some(RepositoryState::RevertSequence) + } else { + Some(RepositoryState::Revert) + } + } else { + None + } + } +} diff --git a/git-repository/src/types.rs b/git-repository/src/types.rs index ae35f24daf0..8cb0bb5826c 100644 --- a/git-repository/src/types.rs +++ b/git-repository/src/types.rs @@ -124,6 +124,31 @@ pub struct Repository { pub(crate) config: crate::config::Cache, } +/// The state of a git repository +#[derive(Debug, PartialEq)] +pub enum RepositoryState { + /// Apply mailbox in progress + ApplyMailbox, + /// Rebase while an apply mailbox operation is in progress + ApplyMailboxRebase, + /// Bisect in progress + Bisect, + /// Cherry pick operation in progress + CherryPick, + /// Cherry pick with multiple commits pending in the sequencer in progress + CherryPickSequence, + /// Merge operation in progress + Merge, + /// Rebase in progress + Rebase, + /// Interactive rebase in progress + RebaseInteractive, + /// Revert operation in progress + Revert, + /// Revert operation with multiple commits pending in the sequencer in progress + RevertSequence, +} + /// An instance with access to everything a git repository entails, best imagined as container implementing `Sync + Send` for _most_ /// for system resources required to interact with a `git` repository which are loaded in once the instance is created. /// diff --git a/git-repository/tests/fixtures/make_cherry_pick_repo.sh b/git-repository/tests/fixtures/make_cherry_pick_repo.sh new file mode 100644 index 00000000000..b8b2ab57509 --- /dev/null +++ b/git-repository/tests/fixtures/make_cherry_pick_repo.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -eu -o pipefail + +git init -q + +git config commit.gpgsign false + +git config advice.statusHints false +git config advice.resolveConflict false +git config advice.commitBeforeMerge false +git config advice.skippedCherryPicks false + +git config init.defaultBranch master + +unset GIT_AUTHOR_DATE +unset GIT_COMMITTER_DATE + +touch 1 +git add 1 +git commit -m 1 1 +git checkout -b other-branch +echo other-branch > 1 +git add 1 +git commit -m 1.other 1 +git checkout master +echo master > 1 +git add 1 +git commit -m 1.master 1 + +# This should fail and leave us in a cherry-pick state +git cherry-pick other-branch || true diff --git a/git-repository/tests/fixtures/make_rebase_i_repo.sh b/git-repository/tests/fixtures/make_rebase_i_repo.sh new file mode 100644 index 00000000000..56e4799ed05 --- /dev/null +++ b/git-repository/tests/fixtures/make_rebase_i_repo.sh @@ -0,0 +1,41 @@ +#!/bin/bash +set -eu -o pipefail + +git init -q + +git config commit.gpgsign false + +git config advice.statusHints false +git config advice.resolveConflict false +git config advice.commitBeforeMerge false +git config advice.skippedCherryPicks false + +git config init.defaultBranch master + +unset GIT_AUTHOR_DATE +unset GIT_COMMITTER_DATE + +touch 1 2 3 +git add 1 +git commit -m 1 1 +git add 2 +git commit -m 2 2 +git add 3 +git commit -m 3 3 + +# NOTE: This relies on GNU sed behavior and will fail on *BSDs (including macOS) without GNU +# sed installed. +sed=$(which gsed sed | head -1 || true) + +# GNU sed recognizes long arguments, BSD sed does not +# NOTE: We can't rely on $? because set -e guarantees the script will terminate on a non-zero exit +${sed} --version 2&>/dev/null && sed_exit_code=success || sed_exit_code=fail +if [ "${sed_exit_code}" = "fail" ]; then + printf "\n** GNU sed is required for this test but was not found **\n" + exit 1 +fi +unset sed_exit_code + +# NOTE: Starting around git 2.35.0 --preserve-merges was renamed to --rebase-merges +# however --preserve-merges first appeared in git 2.18. That should cover most use cases. +EDITOR="${sed} -i.bak -z 's/pick/edit/2'" git rebase --rebase-merges --interactive HEAD~2 diff --git a/git-repository/tests/fixtures/make_revert_repo.sh b/git-repository/tests/fixtures/make_revert_repo.sh new file mode 100644 index 00000000000..08d132fb6ee --- /dev/null +++ b/git-repository/tests/fixtures/make_revert_repo.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -eu -o pipefail + +git init -q + +git config commit.gpgsign false + +git config advice.statusHints false +git config advice.resolveConflict false +git config advice.commitBeforeMerge false +git config advice.skippedCherryPicks false + +git config init.defaultBranch master + +unset GIT_AUTHOR_DATE +unset GIT_COMMITTER_DATE + +touch 1 2 3 +git add 1 +git commit -m 1 1 +git add 2 +git commit -m 2 2 +git add 3 +git commit -m 3 3 +git revert --no-commit HEAD~1 diff --git a/git-repository/tests/repo.rs b/git-repository/tests/repo.rs index d42204658a6..6238e77d689 100644 --- a/git-repository/tests/repo.rs +++ b/git-repository/tests/repo.rs @@ -27,3 +27,4 @@ mod discover; mod easy; mod init; mod reference; +mod state; diff --git a/git-repository/tests/state/mod.rs b/git-repository/tests/state/mod.rs new file mode 100644 index 00000000000..15c629e6f71 --- /dev/null +++ b/git-repository/tests/state/mod.rs @@ -0,0 +1,54 @@ +use crate::{repo, Result}; +use anyhow::anyhow; +use git_repository::{bstr::ByteSlice, RepositoryState}; + +// Can we identify that a cherry pick operation is in progress +#[test] +fn cherry_pick() -> Result { + let repo = repo("make_cherry_pick_repo.sh").map(|r| r.to_thread_local())?; + + let head = repo.head()?; + let head_name = head + .referent_name() + .ok_or_else(|| anyhow!("detached head?"))? + .shorten() + .to_str()?; + assert_eq!("master", head_name); + + assert_eq!(Some(RepositoryState::CherryPick), repo.in_progress_operation()); + + Ok(()) +} + +// Can we identify that we're in the middle of an interactive rebase? +#[test] +fn rebase_interactive() -> Result { + let repo = repo("make_rebase_i_repo.sh").map(|r| r.to_thread_local())?; + + let head = repo.head()?; + // TODO: Get rebase head/target + let head_name = head.referent_name(); + assert!(head_name.is_none()); + + assert_eq!(Some(RepositoryState::RebaseInteractive), repo.in_progress_operation()); + + Ok(()) +} + +// Can we identify a revert operation when we see it? +#[test] +fn revert() -> Result { + let repo = repo("make_revert_repo.sh").map(|r| r.to_thread_local())?; + + let head = repo.head()?; + let head_name = head + .referent_name() + .ok_or_else(|| anyhow!("detached head?"))? + .shorten() + .to_str()?; + assert_eq!("master", head_name); + + assert_eq!(Some(RepositoryState::Revert), repo.in_progress_operation()); + + Ok(()) +}