Refactor
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:
@@ -32,7 +32,7 @@ path = "src/lib.rs"
|
|||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "grm"
|
name = "grm"
|
||||||
path = "src/main.rs"
|
path = "src/grm/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,48 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::repo::Repo;
|
use super::repo::RepoConfig;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub trees: Vec<Tree>,
|
pub trees: Trees,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Trees(Vec<Tree>);
|
||||||
|
|
||||||
|
impl Trees {
|
||||||
|
pub fn to_config(self) -> Config {
|
||||||
|
Config { trees: self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_vec(vec: Vec<Tree>) -> Self {
|
||||||
|
Trees(vec)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_vec(self) -> Vec<Tree> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_vec_ref(&self) -> &Vec<Tree> {
|
||||||
|
self.0.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn as_toml(&self) -> Result<String, String> {
|
||||||
|
match toml::to_string(self) {
|
||||||
|
Ok(toml) => Ok(toml),
|
||||||
|
Err(error) => Err(error.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Tree {
|
pub struct Tree {
|
||||||
pub root: String,
|
pub root: String,
|
||||||
pub repos: Option<Vec<Repo>>,
|
pub repos: Option<Vec<RepoConfig>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_config(path: &str) -> Result<Config, String> {
|
pub fn read_config(path: &str) -> Result<Config, String> {
|
||||||
|
|||||||
278
src/grm/main.rs
Normal file
278
src/grm/main.rs
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
use std::process;
|
||||||
|
|
||||||
|
mod cmd;
|
||||||
|
|
||||||
|
use grm::config;
|
||||||
|
use grm::output::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let opts = cmd::parse();
|
||||||
|
|
||||||
|
match opts.subcmd {
|
||||||
|
cmd::SubCommand::Repos(repos) => match repos.action {
|
||||||
|
cmd::ReposAction::Sync(sync) => {
|
||||||
|
let config = match config::read_config(&sync.config) {
|
||||||
|
Ok(config) => config,
|
||||||
|
Err(error) => {
|
||||||
|
print_error(&error);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match grm::sync_trees(config) {
|
||||||
|
Ok(success) => {
|
||||||
|
if !success {
|
||||||
|
process::exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
print_error(&format!("Error syncing trees: {}", error));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd::ReposAction::Status(args) => match &args.config {
|
||||||
|
Some(config_path) => {
|
||||||
|
let config = match config::read_config(config_path) {
|
||||||
|
Ok(config) => config,
|
||||||
|
Err(error) => {
|
||||||
|
print_error(&error);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match grm::table::get_status_table(config) {
|
||||||
|
Ok((tables, errors)) => {
|
||||||
|
for table in tables {
|
||||||
|
println!("{}", table);
|
||||||
|
}
|
||||||
|
for error in errors {
|
||||||
|
print_error(&format!("Error: {}", error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => print_error(&format!("Error getting status: {}", error)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let dir = match std::env::current_dir() {
|
||||||
|
Ok(dir) => dir,
|
||||||
|
Err(error) => {
|
||||||
|
print_error(&format!("Could not open current directory: {}", error));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match grm::table::show_single_repo_status(&dir) {
|
||||||
|
Ok((table, warnings)) => {
|
||||||
|
println!("{}", table);
|
||||||
|
for warning in warnings {
|
||||||
|
print_warning(&warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => print_error(&format!("Error getting status: {}", error)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cmd::ReposAction::Find(find) => {
|
||||||
|
let path = Path::new(&find.path);
|
||||||
|
if !path.exists() {
|
||||||
|
print_error(&format!("Path \"{}\" does not exist", path.display()));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
if !path.is_dir() {
|
||||||
|
print_error(&format!("Path \"{}\" is not a directory", path.display()));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = match path.canonicalize() {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(error) => {
|
||||||
|
print_error(&format!(
|
||||||
|
"Failed to canonicalize path \"{}\". This is a bug. Error message: {}",
|
||||||
|
&path.display(),
|
||||||
|
error
|
||||||
|
));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let found_repos = match grm::find_in_tree(&path) {
|
||||||
|
Ok(repos) => repos,
|
||||||
|
Err(error) => {
|
||||||
|
print_error(&error);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let trees = grm::config::Trees::from_vec(vec![found_repos]);
|
||||||
|
if trees.as_vec_ref().iter().all(|t| match &t.repos {
|
||||||
|
None => false,
|
||||||
|
Some(r) => r.is_empty(),
|
||||||
|
}) {
|
||||||
|
print_warning("No repositories found");
|
||||||
|
} else {
|
||||||
|
let config = trees.to_config();
|
||||||
|
|
||||||
|
let toml = match config.as_toml() {
|
||||||
|
Ok(toml) => toml,
|
||||||
|
Err(error) => {
|
||||||
|
print_error(&format!("Failed converting config to TOML: {}", &error));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
print!("{}", toml);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cmd::SubCommand::Worktree(args) => {
|
||||||
|
let cwd = std::env::current_dir().unwrap_or_else(|error| {
|
||||||
|
print_error(&format!("Could not open current directory: {}", error));
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
match args.action {
|
||||||
|
cmd::WorktreeAction::Add(action_args) => {
|
||||||
|
let track = match &action_args.track {
|
||||||
|
Some(branch) => {
|
||||||
|
let split = branch.split_once('/');
|
||||||
|
|
||||||
|
if split.is_none() ||
|
||||||
|
split.unwrap().0.len() == 0
|
||||||
|
||split.unwrap().1.len() == 0 {
|
||||||
|
print_error("Tracking branch needs to match the pattern <remote>/<branch_name>");
|
||||||
|
process::exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// unwrap() here is safe because we checked for
|
||||||
|
// is_none() explictily before
|
||||||
|
let (remote_name, remote_branch_name) = split.unwrap();
|
||||||
|
|
||||||
|
Some((remote_name, remote_branch_name))
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
match grm::add_worktree(
|
||||||
|
&cwd,
|
||||||
|
&action_args.name,
|
||||||
|
action_args.branch_namespace.as_deref(),
|
||||||
|
track,
|
||||||
|
) {
|
||||||
|
Ok(_) => print_success(&format!("Worktree {} created", &action_args.name)),
|
||||||
|
Err(error) => {
|
||||||
|
print_error(&format!("Error creating worktree: {}", error));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd::WorktreeAction::Delete(action_args) => {
|
||||||
|
let worktree_dir = cwd.join(&action_args.name);
|
||||||
|
let repo = grm::Repo::open(&cwd, true).unwrap_or_else(|error| {
|
||||||
|
print_error(&format!("Error opening repository: {}", error));
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
match repo.remove_worktree(&action_args.name, &worktree_dir, action_args.force)
|
||||||
|
{
|
||||||
|
Ok(_) => print_success(&format!("Worktree {} deleted", &action_args.name)),
|
||||||
|
Err(error) => {
|
||||||
|
match error {
|
||||||
|
grm::WorktreeRemoveFailureReason::Error(msg) => {
|
||||||
|
print_error(&msg);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
grm::WorktreeRemoveFailureReason::Changes(changes) => {
|
||||||
|
print_warning(&format!(
|
||||||
|
"Changes in worktree: {}. Refusing to delete",
|
||||||
|
changes
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd::WorktreeAction::Status(_args) => {
|
||||||
|
let repo = grm::Repo::open(&cwd, true).unwrap_or_else(|error| {
|
||||||
|
print_error(&format!("Error opening repository: {}", error));
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
match grm::table::get_worktree_status_table(&repo, &cwd) {
|
||||||
|
Ok((table, errors)) => {
|
||||||
|
println!("{}", table);
|
||||||
|
for error in errors {
|
||||||
|
print_error(&format!("Error: {}", error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => print_error(&format!("Error getting status: {}", error)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd::WorktreeAction::Convert(_args) => {
|
||||||
|
// Converting works like this:
|
||||||
|
// * Check whether there are uncommitted/unpushed changes
|
||||||
|
// * Move the contents of .git dir to the worktree directory
|
||||||
|
// * Remove all files
|
||||||
|
// * Set `core.bare` to `true`
|
||||||
|
|
||||||
|
let repo = grm::Repo::open(&cwd, false).unwrap_or_else(|error| {
|
||||||
|
if error.kind == grm::RepoErrorKind::NotFound {
|
||||||
|
print_error("Directory does not contain a git repository");
|
||||||
|
} else {
|
||||||
|
print_error(&format!("Opening repository failed: {}", error));
|
||||||
|
}
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
let status = repo.status(false).unwrap_or_else(|error| {
|
||||||
|
print_error(&format!("Failed getting repo changes: {}", error));
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
if status.changes.is_some() {
|
||||||
|
print_error("Changes found in repository, refusing to convert");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
match repo.convert_to_worktree(&cwd) {
|
||||||
|
Ok(_) => print_success("Conversion done"),
|
||||||
|
Err(error) => print_error(&format!("Error during conversion: {}", error)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd::WorktreeAction::Clean(_args) => {
|
||||||
|
let repo = grm::Repo::open(&cwd, true).unwrap_or_else(|error| {
|
||||||
|
if error.kind == grm::RepoErrorKind::NotFound {
|
||||||
|
print_error("Directory does not contain a git repository");
|
||||||
|
} else {
|
||||||
|
print_error(&format!("Opening repository failed: {}", error));
|
||||||
|
}
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
match repo.cleanup_worktrees(&cwd) {
|
||||||
|
Ok(warnings) => {
|
||||||
|
for warning in warnings {
|
||||||
|
print_warning(&warning);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
print_error(&format!("Worktree cleanup failed: {}", error));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for unmanaged_worktree in
|
||||||
|
repo.find_unmanaged_worktrees(&cwd).unwrap_or_else(|error| {
|
||||||
|
print_error(&format!("Failed finding unmanaged worktrees: {}", error));
|
||||||
|
process::exit(1);
|
||||||
|
})
|
||||||
|
{
|
||||||
|
print_warning(&format!(
|
||||||
|
"Found {}, which is not a valid worktree directory!",
|
||||||
|
&unmanaged_worktree
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1128
src/lib.rs
1128
src/lib.rs
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
|||||||
use grm::run;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
run();
|
|
||||||
}
|
|
||||||
891
src/repo.rs
891
src/repo.rs
@@ -13,6 +13,15 @@ pub enum RemoteType {
|
|||||||
File,
|
File,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum WorktreeRemoveFailureReason {
|
||||||
|
Changes(String),
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum GitPushDefaultSetting {
|
||||||
|
Upstream
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum RepoErrorKind {
|
pub enum RepoErrorKind {
|
||||||
NotFound,
|
NotFound,
|
||||||
@@ -53,7 +62,7 @@ fn worktree_setup_default() -> bool {
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Repo {
|
pub struct RepoConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
#[serde(default = "worktree_setup_default")]
|
#[serde(default = "worktree_setup_default")]
|
||||||
@@ -91,10 +100,7 @@ pub struct RepoStatus {
|
|||||||
|
|
||||||
pub head: Option<String>,
|
pub head: Option<String>,
|
||||||
|
|
||||||
// None(_) => Could not get changes (e.g. because it's a worktree setup)
|
pub changes: Option<RepoChanges>,
|
||||||
// Some(None) => No changes
|
|
||||||
// Some(Some(_)) => Changes
|
|
||||||
pub changes: Option<Option<RepoChanges>>,
|
|
||||||
|
|
||||||
pub worktrees: usize,
|
pub worktrees: usize,
|
||||||
|
|
||||||
@@ -181,66 +187,739 @@ pub fn detect_remote_type(remote_url: &str) -> Option<RemoteType> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_repo(path: &Path, is_worktree: bool) -> Result<Repository, RepoError> {
|
pub struct Repo(git2::Repository);
|
||||||
|
pub struct Branch<'a>(git2::Branch<'a>);
|
||||||
|
|
||||||
|
fn convert_libgit2_error(error: git2::Error) -> String {
|
||||||
|
error.message().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Repo {
|
||||||
|
pub fn open(path: &Path, is_worktree: bool) -> Result<Self, RepoError> {
|
||||||
let open_func = match is_worktree {
|
let open_func = match is_worktree {
|
||||||
true => Repository::open_bare,
|
true => Repository::open_bare,
|
||||||
false => Repository::open,
|
false => Repository::open,
|
||||||
};
|
};
|
||||||
let path = match is_worktree {
|
let path = match is_worktree {
|
||||||
true => path.join(super::GIT_MAIN_WORKTREE_DIRECTORY),
|
true => path.join(crate::GIT_MAIN_WORKTREE_DIRECTORY),
|
||||||
false => path.to_path_buf(),
|
false => path.to_path_buf(),
|
||||||
};
|
};
|
||||||
match open_func(path) {
|
match open_func(path) {
|
||||||
Ok(r) => Ok(r),
|
Ok(r) => Ok(Self(r)),
|
||||||
Err(e) => match e.code() {
|
Err(e) => match e.code() {
|
||||||
git2::ErrorCode::NotFound => Err(RepoError::new(RepoErrorKind::NotFound)),
|
git2::ErrorCode::NotFound => Err(RepoError::new(RepoErrorKind::NotFound)),
|
||||||
_ => Err(RepoError::new(RepoErrorKind::Unknown(
|
_ => Err(RepoError::new(RepoErrorKind::Unknown(
|
||||||
e.message().to_string(),
|
convert_libgit2_error(e),
|
||||||
))),
|
))),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_repo(path: &Path, is_worktree: bool) -> Result<Repository, Box<dyn std::error::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 {
|
let repo = match is_worktree {
|
||||||
false => Repository::init(path)?,
|
false => Repository::init(path).map_err(convert_libgit2_error)?,
|
||||||
true => Repository::init_bare(path.join(super::GIT_MAIN_WORKTREE_DIRECTORY))?,
|
true => Repository::init_bare(path.join(crate::GIT_MAIN_WORKTREE_DIRECTORY))
|
||||||
|
.map_err(convert_libgit2_error)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let repo = Repo(repo);
|
||||||
|
|
||||||
if is_worktree {
|
if is_worktree {
|
||||||
repo_set_config_push(&repo, "upstream")?;
|
repo.set_config_push(GitPushDefaultSetting::Upstream)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(repo)
|
Ok(repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_repo_config(repo: &git2::Repository) -> Result<git2::Config, String> {
|
pub fn config(&self) -> Result<git2::Config, String> {
|
||||||
repo.config()
|
self.0.config().map_err(convert_libgit2_error)
|
||||||
.map_err(|error| format!("Failed getting repository configuration: {}", error))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn repo_make_bare(repo: &git2::Repository, value: bool) -> Result<(), String> {
|
pub fn find_worktree(&self, name: &str) -> Result<(), String> {
|
||||||
let mut config = get_repo_config(repo)?;
|
self.0.find_worktree(name).map_err(convert_libgit2_error)?;
|
||||||
|
Ok(())
|
||||||
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> {
|
pub fn prune_worktree(&self, name: &str) -> Result<(), String> {
|
||||||
let mut config = get_repo_config(repo)?;
|
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
|
config
|
||||||
.set_str(super::GIT_CONFIG_PUSH_DEFAULT, value)
|
.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| {
|
.map_err(|error| {
|
||||||
format!(
|
format!(
|
||||||
"Could not set {}: {}",
|
"Could not set {}: {}",
|
||||||
super::GIT_CONFIG_PUSH_DEFAULT,
|
crate::GIT_CONFIG_PUSH_DEFAULT,
|
||||||
error
|
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(
|
pub fn clone_repo(
|
||||||
remote: &Remote,
|
remote: &Remote,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
@@ -248,7 +927,7 @@ pub fn clone_repo(
|
|||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let clone_target = match is_worktree {
|
let clone_target = match is_worktree {
|
||||||
false => path.to_path_buf(),
|
false => path.to_path_buf(),
|
||||||
true => path.join(super::GIT_MAIN_WORKTREE_DIRECTORY),
|
true => path.join(crate::GIT_MAIN_WORKTREE_DIRECTORY),
|
||||||
};
|
};
|
||||||
|
|
||||||
print_action(&format!(
|
print_action(&format!(
|
||||||
@@ -280,163 +959,9 @@ pub fn clone_repo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if is_worktree {
|
if is_worktree {
|
||||||
let repo = open_repo(&clone_target, false)?;
|
let repo = Repo::open(&clone_target, false)?;
|
||||||
repo_set_config_push(&repo, "upstream")?;
|
repo.set_config_push(GitPushDefaultSetting::Upstream)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
311
src/table.rs
Normal file
311
src/table.rs
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
use comfy_table::{Cell, Table};
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn add_table_header(table: &mut Table) {
|
||||||
|
table
|
||||||
|
.load_preset(comfy_table::presets::UTF8_FULL)
|
||||||
|
.apply_modifier(comfy_table::modifiers::UTF8_ROUND_CORNERS)
|
||||||
|
.set_header(vec![
|
||||||
|
Cell::new("Repo"),
|
||||||
|
Cell::new("Worktree"),
|
||||||
|
Cell::new("Status"),
|
||||||
|
Cell::new("Branches"),
|
||||||
|
Cell::new("HEAD"),
|
||||||
|
Cell::new("Remotes"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_repo_status(
|
||||||
|
table: &mut Table,
|
||||||
|
repo_name: &str,
|
||||||
|
repo_handle: &crate::Repo,
|
||||||
|
is_worktree: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let repo_status = repo_handle.status(is_worktree)?;
|
||||||
|
|
||||||
|
table.add_row(vec![
|
||||||
|
repo_name,
|
||||||
|
match is_worktree {
|
||||||
|
true => "\u{2714}",
|
||||||
|
false => "",
|
||||||
|
},
|
||||||
|
&match repo_status.changes {
|
||||||
|
Some(changes) => {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
if changes.files_new > 0 {
|
||||||
|
out.push(format!("New: {}\n", changes.files_new))
|
||||||
|
}
|
||||||
|
if changes.files_modified > 0 {
|
||||||
|
out.push(format!("Modified: {}\n", changes.files_modified))
|
||||||
|
}
|
||||||
|
if changes.files_deleted > 0 {
|
||||||
|
out.push(format!("Deleted: {}\n", changes.files_deleted))
|
||||||
|
}
|
||||||
|
out.into_iter().collect::<String>().trim().to_string()
|
||||||
|
}
|
||||||
|
None => String::from("\u{2714}"),
|
||||||
|
},
|
||||||
|
&repo_status
|
||||||
|
.branches
|
||||||
|
.iter()
|
||||||
|
.map(|(branch_name, remote_branch)| {
|
||||||
|
format!(
|
||||||
|
"branch: {}{}\n",
|
||||||
|
&branch_name,
|
||||||
|
&match remote_branch {
|
||||||
|
None => String::from(" <!local>"),
|
||||||
|
Some((remote_branch_name, remote_tracking_status)) => {
|
||||||
|
format!(
|
||||||
|
" <{}>{}",
|
||||||
|
remote_branch_name,
|
||||||
|
&match remote_tracking_status {
|
||||||
|
crate::RemoteTrackingStatus::UpToDate =>
|
||||||
|
String::from(" \u{2714}"),
|
||||||
|
crate::RemoteTrackingStatus::Ahead(d) => format!(" [+{}]", &d),
|
||||||
|
crate::RemoteTrackingStatus::Behind(d) => format!(" [-{}]", &d),
|
||||||
|
crate::RemoteTrackingStatus::Diverged(d1, d2) =>
|
||||||
|
format!(" [+{}/-{}]", &d1, &d2),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<String>()
|
||||||
|
.trim()
|
||||||
|
.to_string(),
|
||||||
|
&match is_worktree {
|
||||||
|
true => String::from(""),
|
||||||
|
false => match repo_status.head {
|
||||||
|
Some(head) => head,
|
||||||
|
None => String::from("Empty"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&repo_status
|
||||||
|
.remotes
|
||||||
|
.iter()
|
||||||
|
.map(|r| format!("{}\n", r))
|
||||||
|
.collect::<String>()
|
||||||
|
.trim()
|
||||||
|
.to_string(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't return table, return a type that implements Display(?)
|
||||||
|
pub fn get_worktree_status_table(
|
||||||
|
repo: &crate::Repo,
|
||||||
|
directory: &Path,
|
||||||
|
) -> Result<(impl std::fmt::Display, Vec<String>), String> {
|
||||||
|
let worktrees = repo.get_worktrees()?;
|
||||||
|
let mut table = Table::new();
|
||||||
|
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
add_worktree_table_header(&mut table);
|
||||||
|
for worktree in &worktrees {
|
||||||
|
let worktree_dir = &directory.join(&worktree);
|
||||||
|
if worktree_dir.exists() {
|
||||||
|
let repo = match crate::Repo::open(worktree_dir, false) {
|
||||||
|
Ok(repo) => repo,
|
||||||
|
Err(error) => {
|
||||||
|
errors.push(format!(
|
||||||
|
"Failed opening repo of worktree {}: {}",
|
||||||
|
&worktree, &error
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(error) = add_worktree_status(&mut table, worktree, &repo) {
|
||||||
|
errors.push(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errors.push(format!("Worktree {} does not have a directory", &worktree));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
// this unwrap is safe, as we can be sure that each subentry of
|
||||||
|
// &directory also has the prefix &dir
|
||||||
|
.unwrap()
|
||||||
|
.to_path_buf(),
|
||||||
|
);
|
||||||
|
if dirname == crate::GIT_MAIN_WORKTREE_DIRECTORY {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !&worktrees.contains(&dirname) {
|
||||||
|
errors.push(format!(
|
||||||
|
"Found {}, which is not a valid worktree directory!",
|
||||||
|
&dirname
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((table, errors))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_status_table(config: crate::Config) -> Result<(Vec<Table>, Vec<String>), String> {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
let mut tables = Vec::new();
|
||||||
|
for tree in config.trees.as_vec() {
|
||||||
|
let repos = tree.repos.unwrap_or_default();
|
||||||
|
|
||||||
|
let root_path = crate::expand_path(Path::new(&tree.root));
|
||||||
|
|
||||||
|
let mut table = Table::new();
|
||||||
|
add_table_header(&mut table);
|
||||||
|
|
||||||
|
for repo in &repos {
|
||||||
|
let repo_path = root_path.join(&repo.name);
|
||||||
|
|
||||||
|
if !repo_path.exists() {
|
||||||
|
errors.push(format!(
|
||||||
|
"{}: Repository does not exist. Run sync?",
|
||||||
|
&repo.name
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let repo_handle = crate::Repo::open(&repo_path, repo.worktree_setup);
|
||||||
|
|
||||||
|
let repo_handle = match repo_handle {
|
||||||
|
Ok(repo) => repo,
|
||||||
|
Err(error) => {
|
||||||
|
if error.kind == crate::RepoErrorKind::NotFound {
|
||||||
|
errors.push(format!(
|
||||||
|
"{}: No git repository found. Run sync?",
|
||||||
|
&repo.name
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
errors.push(format!(
|
||||||
|
"{}: Opening repository failed: {}",
|
||||||
|
&repo.name, error
|
||||||
|
));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
add_repo_status(&mut table, &repo.name, &repo_handle, repo.worktree_setup)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
tables.push(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((tables, errors))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_worktree_table_header(table: &mut Table) {
|
||||||
|
table
|
||||||
|
.load_preset(comfy_table::presets::UTF8_FULL)
|
||||||
|
.apply_modifier(comfy_table::modifiers::UTF8_ROUND_CORNERS)
|
||||||
|
.set_header(vec![
|
||||||
|
Cell::new("Worktree"),
|
||||||
|
Cell::new("Status"),
|
||||||
|
Cell::new("Branch"),
|
||||||
|
Cell::new("Remote branch"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_worktree_status(
|
||||||
|
table: &mut Table,
|
||||||
|
worktree_name: &str,
|
||||||
|
repo: &crate::Repo,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let repo_status = repo.status(false)?;
|
||||||
|
|
||||||
|
let local_branch = repo
|
||||||
|
.head_branch()
|
||||||
|
.map_err(|error| format!("Failed getting head branch: {}", error))?;
|
||||||
|
|
||||||
|
let upstream_output = match local_branch.upstream() {
|
||||||
|
Ok(remote_branch) => {
|
||||||
|
let remote_branch_name = remote_branch
|
||||||
|
.name()
|
||||||
|
.map_err(|error| format!("Failed getting name of remote branch: {}", error))?;
|
||||||
|
|
||||||
|
let (ahead, behind) = repo
|
||||||
|
.graph_ahead_behind(&local_branch, &remote_branch)
|
||||||
|
.map_err(|error| format!("Failed computing branch deviation: {}", error))?;
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{}{}\n",
|
||||||
|
&remote_branch_name,
|
||||||
|
&match (ahead, behind) {
|
||||||
|
(0, 0) => String::from(""),
|
||||||
|
(d, 0) => format!(" [+{}]", &d),
|
||||||
|
(0, d) => format!(" [-{}]", &d),
|
||||||
|
(d1, d2) => format!(" [+{}/-{}]", &d1, &d2),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Err(_) => String::from(""),
|
||||||
|
};
|
||||||
|
|
||||||
|
table.add_row(vec![
|
||||||
|
worktree_name,
|
||||||
|
&match repo_status.changes {
|
||||||
|
Some(changes) => {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
if changes.files_new > 0 {
|
||||||
|
out.push(format!("New: {}\n", changes.files_new))
|
||||||
|
}
|
||||||
|
if changes.files_modified > 0 {
|
||||||
|
out.push(format!("Modified: {}\n", changes.files_modified))
|
||||||
|
}
|
||||||
|
if changes.files_deleted > 0 {
|
||||||
|
out.push(format!("Deleted: {}\n", changes.files_deleted))
|
||||||
|
}
|
||||||
|
out.into_iter().collect::<String>().trim().to_string()
|
||||||
|
}
|
||||||
|
None => String::from("\u{2714}"),
|
||||||
|
},
|
||||||
|
&local_branch
|
||||||
|
.name()
|
||||||
|
.map_err(|error| format!("Failed getting name of branch: {}", error))?,
|
||||||
|
&upstream_output,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_single_repo_status(path: &Path) -> Result<(impl std::fmt::Display, Vec<String>), String> {
|
||||||
|
let mut table = Table::new();
|
||||||
|
let mut warnings = Vec::new();
|
||||||
|
|
||||||
|
let is_worktree = crate::Repo::detect_worktree(path);
|
||||||
|
add_table_header(&mut table);
|
||||||
|
|
||||||
|
let repo_handle = crate::Repo::open(path, is_worktree);
|
||||||
|
|
||||||
|
if let Err(error) = repo_handle {
|
||||||
|
if error.kind == crate::RepoErrorKind::NotFound {
|
||||||
|
return Err(String::from("Directory is not a git directory"));
|
||||||
|
} else {
|
||||||
|
return Err(format!("Opening repository failed: {}", error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let repo_name = match path.file_name() {
|
||||||
|
None => {
|
||||||
|
warnings.push(format!("Cannot detect repo name for path {}. Are you working in /?", &path.display()));
|
||||||
|
String::from("unknown")
|
||||||
|
}
|
||||||
|
Some(file_name) => match file_name.to_str() {
|
||||||
|
None => {
|
||||||
|
warnings.push(format!("Name of repo directory {} is not valid UTF-8", &path.display()));
|
||||||
|
String::from("invalid")
|
||||||
|
}
|
||||||
|
Some(name) => name.to_string(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
add_repo_status(&mut table, &repo_name, &repo_handle.unwrap(), is_worktree)?;
|
||||||
|
|
||||||
|
Ok((table, warnings))
|
||||||
|
}
|
||||||
@@ -8,13 +8,13 @@ use helpers::*;
|
|||||||
fn open_empty_repo() {
|
fn open_empty_repo() {
|
||||||
let tmpdir = init_tmpdir();
|
let tmpdir = init_tmpdir();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
open_repo(tmpdir.path(), true),
|
RepoHandle::open(tmpdir.path(), true),
|
||||||
Err(RepoError {
|
Err(RepoError {
|
||||||
kind: RepoErrorKind::NotFound
|
kind: RepoErrorKind::NotFound
|
||||||
})
|
})
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
open_repo(tmpdir.path(), false),
|
RepoHandle::open(tmpdir.path(), false),
|
||||||
Err(RepoError {
|
Err(RepoError {
|
||||||
kind: RepoErrorKind::NotFound
|
kind: RepoErrorKind::NotFound
|
||||||
})
|
})
|
||||||
@@ -25,7 +25,7 @@ fn open_empty_repo() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn create_repo() -> Result<(), Box<dyn std::error::Error>> {
|
fn create_repo() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let tmpdir = init_tmpdir();
|
let tmpdir = init_tmpdir();
|
||||||
let repo = init_repo(tmpdir.path(), false)?;
|
let repo = RepoHandle::init(tmpdir.path(), false)?;
|
||||||
assert!(!repo.is_bare());
|
assert!(!repo.is_bare());
|
||||||
assert!(repo.is_empty()?);
|
assert!(repo.is_empty()?);
|
||||||
cleanup_tmpdir(tmpdir);
|
cleanup_tmpdir(tmpdir);
|
||||||
@@ -35,7 +35,7 @@ fn create_repo() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
#[test]
|
#[test]
|
||||||
fn create_repo_with_worktree() -> Result<(), Box<dyn std::error::Error>> {
|
fn create_repo_with_worktree() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let tmpdir = init_tmpdir();
|
let tmpdir = init_tmpdir();
|
||||||
let repo = init_repo(tmpdir.path(), true)?;
|
let repo = RepoHandle::init(tmpdir.path(), true)?;
|
||||||
assert!(repo.is_bare());
|
assert!(repo.is_bare());
|
||||||
assert!(repo.is_empty()?);
|
assert!(repo.is_empty()?);
|
||||||
cleanup_tmpdir(tmpdir);
|
cleanup_tmpdir(tmpdir);
|
||||||
|
|||||||
Reference in New Issue
Block a user