This refactors a huge chunk of the code base to make it more maintainable.

Main points:

* Proper separation between bin and lib. Bin handles argument parsing &
  validation and (most of) the output. Lib provides interfaces for all
  opreations.

* Before, libgit2 internals were literred throughout the codebase,
  mainly the `Repository` struct and `git2::Error` in Results. They
  library is now properly wrapped in `repo.rs`, which exposes only the
  required functionality. It also standardizes the Error messages
  (they're just Strings for now) and handles stuff like the copious
  usage of Options to wrap maybe-invalid-utf-8 values. The program will
  still panic on non-utf-8 Strings e.g. in git remotes, but I guess this
  is acceptable. If you actually manage to hit this case, I promise I'll
  fix it :D

* Many unwraps() are now gone and properly handled.

* The table printing functionality is now confined to `table.rs`,
  instead of passing tables as parameters through the whole program.
This commit is contained in:
2021-11-30 18:11:33 +01:00
parent da601c2d5f
commit f0c8805cf3
9 changed files with 1629 additions and 1278 deletions

View File

@@ -13,6 +13,15 @@ pub enum RemoteType {
File,
}
pub enum WorktreeRemoveFailureReason {
Changes(String),
Error(String),
}
pub enum GitPushDefaultSetting {
Upstream
}
#[derive(Debug, PartialEq)]
pub enum RepoErrorKind {
NotFound,
@@ -53,7 +62,7 @@ fn worktree_setup_default() -> bool {
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Repo {
pub struct RepoConfig {
pub name: String,
#[serde(default = "worktree_setup_default")]
@@ -91,10 +100,7 @@ pub struct RepoStatus {
pub head: Option<String>,
// None(_) => Could not get changes (e.g. because it's a worktree setup)
// Some(None) => No changes
// Some(Some(_)) => Changes
pub changes: Option<Option<RepoChanges>>,
pub changes: Option<RepoChanges>,
pub worktrees: usize,
@@ -181,64 +187,737 @@ pub fn detect_remote_type(remote_url: &str) -> Option<RemoteType> {
None
}
pub fn open_repo(path: &Path, is_worktree: bool) -> Result<Repository, RepoError> {
let open_func = match is_worktree {
true => Repository::open_bare,
false => Repository::open,
};
let path = match is_worktree {
true => path.join(super::GIT_MAIN_WORKTREE_DIRECTORY),
false => path.to_path_buf(),
};
match open_func(path) {
Ok(r) => Ok(r),
Err(e) => match e.code() {
git2::ErrorCode::NotFound => Err(RepoError::new(RepoErrorKind::NotFound)),
_ => Err(RepoError::new(RepoErrorKind::Unknown(
e.message().to_string(),
))),
},
}
pub struct Repo(git2::Repository);
pub struct Branch<'a>(git2::Branch<'a>);
fn convert_libgit2_error(error: git2::Error) -> String {
error.message().to_string()
}
pub fn init_repo(path: &Path, is_worktree: bool) -> Result<Repository, Box<dyn std::error::Error>> {
let repo = match is_worktree {
false => Repository::init(path)?,
true => Repository::init_bare(path.join(super::GIT_MAIN_WORKTREE_DIRECTORY))?,
};
if is_worktree {
repo_set_config_push(&repo, "upstream")?;
impl Repo {
pub fn open(path: &Path, is_worktree: bool) -> Result<Self, RepoError> {
let open_func = match is_worktree {
true => Repository::open_bare,
false => Repository::open,
};
let path = match is_worktree {
true => path.join(crate::GIT_MAIN_WORKTREE_DIRECTORY),
false => path.to_path_buf(),
};
match open_func(path) {
Ok(r) => Ok(Self(r)),
Err(e) => match e.code() {
git2::ErrorCode::NotFound => Err(RepoError::new(RepoErrorKind::NotFound)),
_ => Err(RepoError::new(RepoErrorKind::Unknown(
convert_libgit2_error(e),
))),
},
}
}
Ok(repo)
}
pub fn get_repo_config(repo: &git2::Repository) -> Result<git2::Config, String> {
repo.config()
.map_err(|error| format!("Failed getting repository configuration: {}", error))
}
pub fn repo_make_bare(repo: &git2::Repository, value: bool) -> Result<(), String> {
let mut config = get_repo_config(repo)?;
config
.set_bool(super::GIT_CONFIG_BARE_KEY, value)
.map_err(|error| format!("Could not set {}: {}", super::GIT_CONFIG_BARE_KEY, error))
}
pub fn repo_set_config_push(repo: &git2::Repository, value: &str) -> Result<(), String> {
let mut config = get_repo_config(repo)?;
config
.set_str(super::GIT_CONFIG_PUSH_DEFAULT, value)
.map_err(|error| {
format!(
"Could not set {}: {}",
super::GIT_CONFIG_PUSH_DEFAULT,
error
pub fn graph_ahead_behind(
&self,
local_branch: &Branch,
remote_branch: &Branch,
) -> Result<(usize, usize), String> {
self.0
.graph_ahead_behind(
local_branch.commit()?.id().0,
remote_branch.commit()?.id().0,
)
.map_err(convert_libgit2_error)
}
pub fn head_branch(&self) -> Result<Branch, String> {
let head = self.0.head().map_err(convert_libgit2_error)?;
if !head.is_branch() {
return Err(String::from("No branch checked out"));
}
// unwrap() is safe here, as we can be certain that a branch with that
// name exists
let branch = self
.find_local_branch(
&head
.shorthand()
.expect("Branch name is not valid utf-8")
.to_string(),
)
.unwrap();
Ok(branch)
}
pub fn remote_set_url(&self, name: &str, url: &str) -> Result<(), String> {
self.0
.remote_set_url(name, url)
.map_err(convert_libgit2_error)
}
pub fn remote_delete(&self, name: &str) -> Result<(), String> {
self.0.remote_delete(name).map_err(convert_libgit2_error)
}
pub fn is_empty(&self) -> Result<bool, String> {
self.0.is_empty().map_err(convert_libgit2_error)
}
pub fn is_bare(&self) -> bool {
self.0.is_bare()
}
pub fn new_worktree(
&self,
name: &str,
directory: &Path,
target_branch: &Branch,
) -> Result<(), String> {
self.0
.worktree(
name,
directory,
Some(git2::WorktreeAddOptions::new().reference(Some(target_branch.as_reference()))),
)
.map_err(convert_libgit2_error)?;
Ok(())
}
pub fn remotes(&self) -> Result<Vec<String>, String> {
Ok(self
.0
.remotes()
.map_err(convert_libgit2_error)?
.iter()
.map(|name| name.expect("Remote name is invalid utf-8"))
.map(|name| name.to_owned())
.collect())
}
pub fn new_remote(&self, name: &str, url: &str) -> Result<(), String> {
self.0.remote(name, url).map_err(convert_libgit2_error)?;
Ok(())
}
pub fn init(path: &Path, is_worktree: bool) -> Result<Self, String> {
let repo = match is_worktree {
false => Repository::init(path).map_err(convert_libgit2_error)?,
true => Repository::init_bare(path.join(crate::GIT_MAIN_WORKTREE_DIRECTORY))
.map_err(convert_libgit2_error)?,
};
let repo = Repo(repo);
if is_worktree {
repo.set_config_push(GitPushDefaultSetting::Upstream)?;
}
Ok(repo)
}
pub fn config(&self) -> Result<git2::Config, String> {
self.0.config().map_err(convert_libgit2_error)
}
pub fn find_worktree(&self, name: &str) -> Result<(), String> {
self.0.find_worktree(name).map_err(convert_libgit2_error)?;
Ok(())
}
pub fn prune_worktree(&self, name: &str) -> Result<(), String> {
let worktree = self.0.find_worktree(name).map_err(convert_libgit2_error)?;
worktree.prune(None).map_err(convert_libgit2_error)?;
Ok(())
}
pub fn find_remote_branch(&self, remote_name: &str, branch_name: &str) -> Result<Branch, String> {
Ok(Branch(
self.0
.find_branch(&format!("{}/{}", remote_name, branch_name), git2::BranchType::Remote)
.map_err(convert_libgit2_error)?,
))
}
pub fn find_local_branch(&self, name: &str) -> Result<Branch, String> {
Ok(Branch(
self.0
.find_branch(name, git2::BranchType::Local)
.map_err(convert_libgit2_error)?,
))
}
pub fn create_branch(&self, name: &str, target: &Commit) -> Result<Branch, String> {
Ok(Branch(
self.0
.branch(name, &target.0, false)
.map_err(convert_libgit2_error)?,
))
}
pub fn make_bare(&self, value: bool) -> Result<(), String> {
let mut config = self.config()?;
config
.set_bool(crate::GIT_CONFIG_BARE_KEY, value)
.map_err(|error| format!("Could not set {}: {}", crate::GIT_CONFIG_BARE_KEY, error))
}
pub fn convert_to_worktree(&self, root_dir: &Path) -> Result<(), String> {
std::fs::rename(".git", crate::GIT_MAIN_WORKTREE_DIRECTORY)
.map_err(|error| format!("Error moving .git directory: {}", error))?;
for entry in match std::fs::read_dir(&root_dir) {
Ok(iterator) => iterator,
Err(error) => {
return Err(format!("Opening directory failed: {}", error));
}
} {
match entry {
Ok(entry) => {
let path = entry.path();
// unwrap is safe here, the path will ALWAYS have a file component
if path.file_name().unwrap() == crate::GIT_MAIN_WORKTREE_DIRECTORY {
continue;
}
if path.is_file() || path.is_symlink() {
if let Err(error) = std::fs::remove_file(&path) {
return Err(format!("Failed removing {}", error));
}
} else if let Err(error) = std::fs::remove_dir_all(&path) {
return Err(format!("Failed removing {}", error));
}
}
Err(error) => {
return Err(format!("Error getting directory entry: {}", error));
}
}
}
let worktree_repo = Repo::open(root_dir, true)
.map_err(|error| format!("Opening newly converted repository failed: {}", error))?;
worktree_repo
.make_bare(true)
.map_err(|error| format!("Error: {}", error))?;
worktree_repo
.set_config_push(GitPushDefaultSetting::Upstream)
.map_err(|error| format!("Error: {}", error))?;
Ok(())
}
pub fn set_config_push(&self, value: GitPushDefaultSetting) -> Result<(), String> {
let mut config = self.config()?;
config
.set_str(crate::GIT_CONFIG_PUSH_DEFAULT, match value {
GitPushDefaultSetting::Upstream => "upstream",
})
.map_err(|error| {
format!(
"Could not set {}: {}",
crate::GIT_CONFIG_PUSH_DEFAULT,
error
)
})
}
pub fn status(&self, is_worktree: bool) -> Result<RepoStatus, String> {
let operation = match self.0.state() {
git2::RepositoryState::Clean => None,
state => Some(state),
};
let empty = self.is_empty()?;
let remotes = self
.0
.remotes()
.map_err(convert_libgit2_error)?
.iter()
.map(|repo_name| repo_name.expect("Worktree name is invalid utf-8."))
.map(|repo_name| repo_name.to_owned())
.collect::<Vec<String>>();
let head = match is_worktree {
true => None,
false => match empty {
true => None,
false => Some(self.head_branch()?.name()?),
},
};
let changes = match is_worktree {
true => {
return Err(String::from(
"Cannot get changes as this is a bare worktree repository",
))
}
false => {
let statuses = self
.0
.statuses(Some(
git2::StatusOptions::new()
.include_ignored(false)
.include_untracked(true),
))
.map_err(convert_libgit2_error)?;
match statuses.is_empty() {
true => None,
false => {
let mut files_new = 0;
let mut files_modified = 0;
let mut files_deleted = 0;
for status in statuses.iter() {
let status_bits = status.status();
if status_bits.intersects(
git2::Status::INDEX_MODIFIED
| git2::Status::INDEX_RENAMED
| git2::Status::INDEX_TYPECHANGE
| git2::Status::WT_MODIFIED
| git2::Status::WT_RENAMED
| git2::Status::WT_TYPECHANGE,
) {
files_modified += 1;
} else if status_bits
.intersects(git2::Status::INDEX_NEW | git2::Status::WT_NEW)
{
files_new += 1;
} else if status_bits
.intersects(git2::Status::INDEX_DELETED | git2::Status::WT_DELETED)
{
files_deleted += 1;
}
}
if (files_new, files_modified, files_deleted) == (0, 0, 0) {
panic!(
"is_empty() returned true, but no file changes were detected. This is a bug!"
);
}
Some(RepoChanges {
files_new,
files_modified,
files_deleted,
})
}
}
}
};
let worktrees = self.0.worktrees().unwrap().len();
let submodules = match is_worktree {
true => None,
false => {
let mut submodules = Vec::new();
for submodule in self.0.submodules().unwrap() {
let submodule_name = submodule.name().unwrap().to_string();
let submodule_status;
let status = self
.0
.submodule_status(submodule.name().unwrap(), git2::SubmoduleIgnore::None)
.unwrap();
if status.intersects(
git2::SubmoduleStatus::WD_INDEX_MODIFIED
| git2::SubmoduleStatus::WD_WD_MODIFIED
| git2::SubmoduleStatus::WD_UNTRACKED,
) {
submodule_status = SubmoduleStatus::Changed;
} else if status.is_wd_uninitialized() {
submodule_status = SubmoduleStatus::Uninitialized;
} else if status.is_wd_modified() {
submodule_status = SubmoduleStatus::OutOfDate;
} else {
submodule_status = SubmoduleStatus::Clean;
}
submodules.push((submodule_name, submodule_status));
}
Some(submodules)
}
};
let mut branches = Vec::new();
for (local_branch, _) in self
.0
.branches(Some(git2::BranchType::Local))
.unwrap()
.map(|branch_name| branch_name.unwrap())
{
let branch_name = local_branch.name().unwrap().unwrap().to_string();
let remote_branch = match local_branch.upstream() {
Ok(remote_branch) => {
let remote_branch_name = remote_branch.name().unwrap().unwrap().to_string();
let (ahead, behind) = self
.0
.graph_ahead_behind(
local_branch.get().peel_to_commit().unwrap().id(),
remote_branch.get().peel_to_commit().unwrap().id(),
)
.unwrap();
let remote_tracking_status = match (ahead, behind) {
(0, 0) => RemoteTrackingStatus::UpToDate,
(0, d) => RemoteTrackingStatus::Behind(d),
(d, 0) => RemoteTrackingStatus::Ahead(d),
(d1, d2) => RemoteTrackingStatus::Diverged(d1, d2),
};
Some((remote_branch_name, remote_tracking_status))
}
// Err => no remote branch
Err(_) => None,
};
branches.push((branch_name, remote_branch));
}
Ok(RepoStatus {
operation,
empty,
remotes,
head,
changes,
worktrees,
submodules,
branches,
})
}
pub fn default_branch(&self) -> Result<Branch, String> {
match self.0.find_branch("main", git2::BranchType::Local) {
Ok(branch) => Ok(Branch(branch)),
Err(_) => match self.0.find_branch("master", git2::BranchType::Local) {
Ok(branch) => Ok(Branch(branch)),
Err(_) => Err(String::from("Could not determine default branch")),
},
}
}
// Looks like there is no distinguishing between the error cases
// "no such remote" and "failed to get remote for some reason".
// May be a good idea to handle this explicitly, by returning a
// Result<Option<RemoteHandle>, String> instead, Returning Ok(None)
// on "not found" and Err() on an actual error.
pub fn find_remote(&self, remote_name: &str) -> Result<Option<RemoteHandle>, String> {
let remotes = self.0.remotes().map_err(convert_libgit2_error)?;
if !remotes.iter().any(|remote| remote.expect("Remote name is invalid utf-8") == remote_name) {
return Ok(None)
}
Ok(Some(RemoteHandle(
self.0
.find_remote(remote_name)
.map_err(convert_libgit2_error)?,
)))
}
pub fn get_worktrees(&self) -> Result<Vec<String>, String> {
Ok(self
.0
.worktrees()
.map_err(convert_libgit2_error)?
.iter()
.map(|name| name.expect("Worktree name is invalid utf-8"))
.map(|name| name.to_string())
.collect())
}
pub fn remove_worktree(
&self,
name: &str,
worktree_dir: &Path,
force: bool,
) -> Result<(), WorktreeRemoveFailureReason> {
if !worktree_dir.exists() {
return Err(WorktreeRemoveFailureReason::Error(format!(
"{} does not exist",
name
)));
}
let worktree_repo = Repo::open(worktree_dir, false).map_err(|error| {
WorktreeRemoveFailureReason::Error(format!("Error opening repo: {}", error))
})?;
let local_branch = worktree_repo.head_branch().map_err(|error| {
WorktreeRemoveFailureReason::Error(format!("Failed getting head branch: {}", error))
})?;
let branch_name = local_branch.name().map_err(|error| {
WorktreeRemoveFailureReason::Error(format!("Failed getting name of branch: {}", error))
})?;
if branch_name != name
&& !branch_name.ends_with(&format!("{}{}", crate::BRANCH_NAMESPACE_SEPARATOR, name))
{
return Err(WorktreeRemoveFailureReason::Error(format!(
"Branch {} is checked out in worktree, this does not look correct",
&branch_name
)));
}
let branch = worktree_repo
.find_local_branch(&branch_name)
.map_err(WorktreeRemoveFailureReason::Error)?;
if !force {
let status = worktree_repo
.status(false)
.map_err(WorktreeRemoveFailureReason::Error)?;
if status.changes.is_some() {
return Err(WorktreeRemoveFailureReason::Changes(String::from(
"Changes found in worktree",
)));
}
match branch.upstream() {
Ok(remote_branch) => {
let (ahead, behind) = worktree_repo
.graph_ahead_behind(&branch, &remote_branch)
.unwrap();
if (ahead, behind) != (0, 0) {
return Err(WorktreeRemoveFailureReason::Changes(format!(
"Branch {} is not in line with remote branch",
name
)));
}
}
Err(_) => {
return Err(WorktreeRemoveFailureReason::Changes(format!(
"No remote tracking branch for branch {} found",
name
)));
}
}
}
if let Err(e) = std::fs::remove_dir_all(&worktree_dir) {
return Err(WorktreeRemoveFailureReason::Error(format!(
"Error deleting {}: {}",
&worktree_dir.display(),
e
)));
}
self.prune_worktree(name)
.map_err(WorktreeRemoveFailureReason::Error)?;
branch
.delete()
.map_err(WorktreeRemoveFailureReason::Error)?;
Ok(())
}
pub fn cleanup_worktrees(&self, directory: &Path) -> Result<Vec<String>, String> {
let mut warnings = Vec::new();
let worktrees = self
.get_worktrees()
.map_err(|error| format!("Getting worktrees failed: {}", error))?;
let default_branch = self
.default_branch()
.map_err(|error| format!("Failed getting default branch: {}", error))?;
let default_branch_name = default_branch
.name()
.map_err(|error| format!("Failed getting default branch name: {}", error))?;
for worktree in worktrees
.iter()
.filter(|worktree| *worktree != &default_branch_name)
{
let repo_dir = &directory.join(&worktree);
if repo_dir.exists() {
match self.remove_worktree(worktree, repo_dir, false) {
Ok(_) => print_success(&format!("Worktree {} deleted", &worktree)),
Err(error) => match error {
WorktreeRemoveFailureReason::Changes(changes) => {
warnings.push(format!(
"Changes found in {}: {}, skipping",
&worktree, &changes
));
continue;
}
WorktreeRemoveFailureReason::Error(error) => {
return Err(error);
}
},
}
} else {
warnings.push(format!("Worktree {} does not have a directory", &worktree));
}
}
Ok(warnings)
}
pub fn find_unmanaged_worktrees(&self, directory: &Path) -> Result<Vec<String>, String> {
let worktrees = self
.get_worktrees()
.map_err(|error| format!("Getting worktrees failed: {}", error))?;
let mut unmanaged_worktrees = Vec::new();
for entry in std::fs::read_dir(&directory).map_err(|error| error.to_string())? {
let dirname = crate::path_as_string(
&entry.map_err(|error| error.to_string())?
.path()
.strip_prefix(&directory)
// that unwrap() is safe as each entry is
// guaranteed to be a subentry of &directory
.unwrap()
.to_path_buf(),
);
let default_branch = self
.default_branch()
.map_err(|error| format!("Failed getting default branch: {}", error))?;
let default_branch_name = default_branch
.name()
.map_err(|error| format!("Failed getting default branch name: {}", error))?;
if dirname == crate::GIT_MAIN_WORKTREE_DIRECTORY {
continue;
}
if dirname == default_branch_name {
continue;
}
if !&worktrees.contains(&dirname) {
unmanaged_worktrees.push(dirname);
}
}
Ok(unmanaged_worktrees)
}
pub fn detect_worktree(path: &Path) -> bool {
path.join(crate::GIT_MAIN_WORKTREE_DIRECTORY).exists()
}
}
pub struct RemoteHandle<'a>(git2::Remote<'a>);
pub struct Commit<'a>(git2::Commit<'a>);
pub struct Reference<'a>(git2::Reference<'a>);
pub struct Oid(git2::Oid);
impl Oid {
pub fn hex_string(&self) -> String {
self.0.to_string()
}
}
impl Commit<'_> {
pub fn id(&self) -> Oid {
Oid(self.0.id())
}
}
impl<'a> Branch<'a> {
pub fn to_commit(self) -> Result<Commit<'a>, String> {
Ok(Commit(
self.0
.into_reference()
.peel_to_commit()
.map_err(convert_libgit2_error)?,
))
}
}
impl Branch<'_> {
pub fn commit(&self) -> Result<Commit, String> {
Ok(Commit(
self.0
.get()
.peel_to_commit()
.map_err(convert_libgit2_error)?,
))
}
pub fn set_upstream(&mut self, remote_name: &str, branch_name: &str) -> Result<(), String> {
self.0
.set_upstream(Some(&format!("{}/{}", remote_name, branch_name)))
.map_err(convert_libgit2_error)?;
Ok(())
}
pub fn name(&self) -> Result<String, String> {
self.0
.name()
.map(|name| name.expect("Branch name is invalid utf-8"))
.map_err(convert_libgit2_error)
.map(|name| name.to_string())
}
pub fn upstream(&self) -> Result<Branch, String> {
Ok(Branch(self.0.upstream().map_err(convert_libgit2_error)?))
}
pub fn delete(mut self) -> Result<(), String> {
self.0.delete().map_err(convert_libgit2_error)
}
// only used internally in this module, exposes libgit2 details
fn as_reference(&self) -> &git2::Reference {
self.0.get()
}
}
impl RemoteHandle<'_> {
pub fn url(&self) -> String {
self.0
.url()
.expect("Remote URL is invalid utf-8")
.to_string()
}
pub fn name(&self) -> String {
self.0
.name()
.expect("Remote name is invalid utf-8")
.to_string()
}
pub fn push(
&mut self,
local_branch_name: &str,
remote_branch_name: &str,
_repo: &Repo,
) -> Result<(), String> {
let mut callbacks = git2::RemoteCallbacks::new();
callbacks.push_update_reference(|_, status| {
if let Some(message) = status {
return Err(git2::Error::new(
git2::ErrorCode::GenericError,
git2::ErrorClass::None,
message,
));
}
Ok(())
});
callbacks.credentials(|_url, username_from_url, _allowed_types| {
git2::Cred::ssh_key_from_agent(username_from_url.unwrap())
});
let mut push_options = git2::PushOptions::new();
push_options.remote_callbacks(callbacks);
let push_refspec = format!(
"+refs/heads/{}:refs/heads/{}",
local_branch_name, remote_branch_name
);
self.0
.push(&[push_refspec], Some(&mut push_options))
.map_err(|error| {
format!(
"Pushing {} to {} ({}) failed: {}",
local_branch_name,
self.name(),
self.url(),
error
)
})?;
Ok(())
}
}
pub fn clone_repo(
@@ -248,7 +927,7 @@ pub fn clone_repo(
) -> Result<(), Box<dyn std::error::Error>> {
let clone_target = match is_worktree {
false => path.to_path_buf(),
true => path.join(super::GIT_MAIN_WORKTREE_DIRECTORY),
true => path.join(crate::GIT_MAIN_WORKTREE_DIRECTORY),
};
print_action(&format!(
@@ -280,163 +959,9 @@ pub fn clone_repo(
}
if is_worktree {
let repo = open_repo(&clone_target, false)?;
repo_set_config_push(&repo, "upstream")?;
let repo = Repo::open(&clone_target, false)?;
repo.set_config_push(GitPushDefaultSetting::Upstream)?;
}
Ok(())
}
pub fn get_repo_status(repo: &git2::Repository, is_worktree: bool) -> RepoStatus {
let operation = match repo.state() {
git2::RepositoryState::Clean => None,
state => Some(state),
};
let empty = repo.is_empty().unwrap();
let remotes = repo
.remotes()
.unwrap()
.iter()
.map(|repo_name| repo_name.unwrap().to_string())
.collect::<Vec<String>>();
let head = match is_worktree {
true => None,
false => match empty {
true => None,
false => Some(repo.head().unwrap().shorthand().unwrap().to_string()),
},
};
let changes = match is_worktree {
true => None,
false => {
let statuses = repo
.statuses(Some(
git2::StatusOptions::new()
.include_ignored(false)
.include_untracked(true),
))
.unwrap();
match statuses.is_empty() {
true => Some(None),
false => {
let mut files_new = 0;
let mut files_modified = 0;
let mut files_deleted = 0;
for status in statuses.iter() {
let status_bits = status.status();
if status_bits.intersects(
git2::Status::INDEX_MODIFIED
| git2::Status::INDEX_RENAMED
| git2::Status::INDEX_TYPECHANGE
| git2::Status::WT_MODIFIED
| git2::Status::WT_RENAMED
| git2::Status::WT_TYPECHANGE,
) {
files_modified += 1;
} else if status_bits
.intersects(git2::Status::INDEX_NEW | git2::Status::WT_NEW)
{
files_new += 1;
} else if status_bits
.intersects(git2::Status::INDEX_DELETED | git2::Status::WT_DELETED)
{
files_deleted += 1;
}
}
if (files_new, files_modified, files_deleted) == (0, 0, 0) {
panic!(
"is_empty() returned true, but no file changes were detected. This is a bug!"
);
}
Some(Some(RepoChanges {
files_new,
files_modified,
files_deleted,
}))
}
}
}
};
let worktrees = repo.worktrees().unwrap().len();
let submodules = match is_worktree {
true => None,
false => {
let mut submodules = Vec::new();
for submodule in repo.submodules().unwrap() {
let submodule_name = submodule.name().unwrap().to_string();
let submodule_status;
let status = repo
.submodule_status(submodule.name().unwrap(), git2::SubmoduleIgnore::None)
.unwrap();
if status.intersects(
git2::SubmoduleStatus::WD_INDEX_MODIFIED
| git2::SubmoduleStatus::WD_WD_MODIFIED
| git2::SubmoduleStatus::WD_UNTRACKED,
) {
submodule_status = SubmoduleStatus::Changed;
} else if status.is_wd_uninitialized() {
submodule_status = SubmoduleStatus::Uninitialized;
} else if status.is_wd_modified() {
submodule_status = SubmoduleStatus::OutOfDate;
} else {
submodule_status = SubmoduleStatus::Clean;
}
submodules.push((submodule_name, submodule_status));
}
Some(submodules)
}
};
let mut branches = Vec::new();
for (local_branch, _) in repo
.branches(Some(git2::BranchType::Local))
.unwrap()
.map(|branch_name| branch_name.unwrap())
{
let branch_name = local_branch.name().unwrap().unwrap().to_string();
let remote_branch = match local_branch.upstream() {
Ok(remote_branch) => {
let remote_branch_name = remote_branch.name().unwrap().unwrap().to_string();
let (ahead, behind) = repo
.graph_ahead_behind(
local_branch.get().peel_to_commit().unwrap().id(),
remote_branch.get().peel_to_commit().unwrap().id(),
)
.unwrap();
let remote_tracking_status = match (ahead, behind) {
(0, 0) => RemoteTrackingStatus::UpToDate,
(0, d) => RemoteTrackingStatus::Behind(d),
(d, 0) => RemoteTrackingStatus::Ahead(d),
(d1, d2) => RemoteTrackingStatus::Diverged(d1, d2),
};
Some((remote_branch_name, remote_tracking_status))
}
// Err => no remote branch
Err(_) => None,
};
branches.push((branch_name, remote_branch));
}
RepoStatus {
operation,
empty,
remotes,
head,
changes,
worktrees,
submodules,
branches,
}
}