mirror of
https://github.com/gitui-org/gitui
synced 2026-05-24 09:28:21 +00:00
386 lines
9.8 KiB
Rust
386 lines
9.8 KiB
Rust
//! sync git api (various methods)
|
|
|
|
use super::CommitId;
|
|
use crate::error::{Error, Result};
|
|
use git2::{IndexAddOption, Repository, RepositoryOpenFlags};
|
|
use scopetime::scope_time;
|
|
use std::{fs::File, io::Write, path::Path};
|
|
|
|
///
|
|
#[derive(PartialEq, Debug, Clone)]
|
|
pub struct Head {
|
|
///
|
|
pub name: String,
|
|
///
|
|
pub id: CommitId,
|
|
}
|
|
|
|
///
|
|
pub fn is_repo(repo_path: &str) -> bool {
|
|
Repository::open_ext(
|
|
repo_path,
|
|
RepositoryOpenFlags::empty(),
|
|
Vec::<&Path>::new(),
|
|
)
|
|
.is_ok()
|
|
}
|
|
|
|
/// checks if the git repo at path `repo_path` is a bare repo
|
|
pub fn is_bare_repo(repo_path: &str) -> Result<bool> {
|
|
let repo = Repository::open_ext(
|
|
repo_path,
|
|
RepositoryOpenFlags::empty(),
|
|
Vec::<&Path>::new(),
|
|
)?;
|
|
|
|
Ok(repo.is_bare())
|
|
}
|
|
|
|
///
|
|
pub(crate) fn repo(repo_path: &str) -> Result<Repository> {
|
|
let repo = Repository::open_ext(
|
|
repo_path,
|
|
RepositoryOpenFlags::empty(),
|
|
Vec::<&Path>::new(),
|
|
)?;
|
|
|
|
if repo.is_bare() {
|
|
return Err(Error::Generic("bare repo".to_string()));
|
|
}
|
|
|
|
Ok(repo)
|
|
}
|
|
|
|
///
|
|
pub(crate) fn work_dir(repo: &Repository) -> Result<&Path> {
|
|
repo.workdir().ok_or(Error::NoWorkDir)
|
|
}
|
|
|
|
///
|
|
pub fn repo_work_dir(repo_path: &str) -> Result<String> {
|
|
let repo = repo(repo_path)?;
|
|
work_dir(&repo)?.to_str().map_or_else(
|
|
|| Err(Error::Generic("invalid workdir".to_string())),
|
|
|workdir| Ok(workdir.to_string()),
|
|
)
|
|
}
|
|
|
|
///
|
|
pub fn get_head(repo_path: &str) -> Result<CommitId> {
|
|
let repo = repo(repo_path)?;
|
|
get_head_repo(&repo)
|
|
}
|
|
|
|
///
|
|
pub fn get_head_tuple(repo_path: &str) -> Result<Head> {
|
|
let repo = repo(repo_path)?;
|
|
let id = get_head_repo(&repo)?;
|
|
let name = get_head_refname(&repo)?;
|
|
|
|
Ok(Head { name, id })
|
|
}
|
|
|
|
///
|
|
pub fn get_head_refname(repo: &Repository) -> Result<String> {
|
|
let head = repo.head()?;
|
|
let ref_name = bytes2string(head.name_bytes())?;
|
|
|
|
Ok(ref_name)
|
|
}
|
|
|
|
///
|
|
pub fn get_head_repo(repo: &Repository) -> Result<CommitId> {
|
|
scope_time!("get_head_repo");
|
|
|
|
let head = repo.head()?.target();
|
|
|
|
head.map_or(Err(Error::NoHead), |head_id| Ok(head_id.into()))
|
|
}
|
|
|
|
/// add a file diff from workingdir to stage (will not add removed files see `stage_addremoved`)
|
|
pub fn stage_add_file(repo_path: &str, path: &Path) -> Result<()> {
|
|
scope_time!("stage_add_file");
|
|
|
|
let repo = repo(repo_path)?;
|
|
|
|
let mut index = repo.index()?;
|
|
|
|
index.add_path(path)?;
|
|
index.write()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// like `stage_add_file` but uses a pattern to match/glob multiple files/folders
|
|
pub fn stage_add_all(repo_path: &str, pattern: &str) -> Result<()> {
|
|
scope_time!("stage_add_all");
|
|
|
|
let repo = repo(repo_path)?;
|
|
|
|
let mut index = repo.index()?;
|
|
|
|
index.add_all(vec![pattern], IndexAddOption::DEFAULT, None)?;
|
|
index.write()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// stage a removed file
|
|
pub fn stage_addremoved(repo_path: &str, path: &Path) -> Result<()> {
|
|
scope_time!("stage_addremoved");
|
|
|
|
let repo = repo(repo_path)?;
|
|
|
|
let mut index = repo.index()?;
|
|
|
|
index.remove_path(path)?;
|
|
index.write()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// get string from config
|
|
pub fn get_config_string(
|
|
repo_path: &str,
|
|
key: &str,
|
|
) -> Result<Option<String>> {
|
|
let repo = repo(repo_path)?;
|
|
let cfg = repo.config()?;
|
|
|
|
// this code doesnt match what the doc says regarding what
|
|
// gets returned when but it actually works
|
|
let entry_res = cfg.get_entry(key);
|
|
|
|
let entry = match entry_res {
|
|
Ok(ent) => ent,
|
|
Err(_) => return Ok(None),
|
|
};
|
|
|
|
if entry.has_value() {
|
|
Ok(entry.value().map(std::string::ToString::to_string))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
pub(crate) fn bytes2string(bytes: &[u8]) -> Result<String> {
|
|
Ok(String::from_utf8(bytes.to_vec())?)
|
|
}
|
|
|
|
/// write a file in repo
|
|
pub(crate) fn repo_write_file(
|
|
repo: &Repository,
|
|
file: &str,
|
|
content: &str,
|
|
) -> Result<()> {
|
|
let dir = work_dir(repo)?.join(file);
|
|
let file_path = dir.to_str().ok_or_else(|| {
|
|
Error::Generic(String::from("invalid file path"))
|
|
})?;
|
|
let mut file = File::create(file_path)?;
|
|
file.write_all(content.as_bytes())?;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn repo_read_file(
|
|
repo: &Repository,
|
|
file: &str,
|
|
) -> Result<String> {
|
|
use std::io::Read;
|
|
|
|
let dir = work_dir(repo)?.join(file);
|
|
let file_path = dir.to_str().ok_or_else(|| {
|
|
Error::Generic(String::from("invalid file path"))
|
|
})?;
|
|
|
|
let mut file = File::open(file_path)?;
|
|
let mut buffer = Vec::new();
|
|
file.read_to_end(&mut buffer)?;
|
|
|
|
Ok(String::from_utf8(buffer)?)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::sync::{
|
|
commit,
|
|
status::{get_status, StatusType},
|
|
tests::{
|
|
debug_cmd_print, get_statuses, repo_init, repo_init_empty,
|
|
},
|
|
};
|
|
use std::{
|
|
fs::{self, remove_file, File},
|
|
io::Write,
|
|
path::Path,
|
|
};
|
|
|
|
#[test]
|
|
fn test_stage_add_smoke() {
|
|
let file_path = Path::new("foo");
|
|
let (_td, repo) = repo_init_empty().unwrap();
|
|
let root = repo.path().parent().unwrap();
|
|
let repo_path = root.as_os_str().to_str().unwrap();
|
|
|
|
assert_eq!(
|
|
stage_add_file(repo_path, file_path).is_ok(),
|
|
false
|
|
);
|
|
}
|
|
#[test]
|
|
fn test_get_config() {
|
|
let bad_dir_cfg =
|
|
get_config_string("oodly_noodly", "this.doesnt.exist");
|
|
assert!(bad_dir_cfg.is_err());
|
|
|
|
let (_td, repo) = repo_init().unwrap();
|
|
let path = repo.path();
|
|
let rpath = path.as_os_str().to_str().unwrap();
|
|
let bad_cfg = get_config_string(rpath, "this.doesnt.exist");
|
|
assert!(bad_cfg.is_ok());
|
|
assert!(bad_cfg.unwrap().is_none());
|
|
// repo init sets user.name
|
|
let good_cfg = get_config_string(rpath, "user.name");
|
|
assert!(good_cfg.is_ok());
|
|
assert!(good_cfg.unwrap().is_some());
|
|
}
|
|
#[test]
|
|
fn test_staging_one_file() {
|
|
let file_path = Path::new("file1.txt");
|
|
let (_td, repo) = repo_init().unwrap();
|
|
let root = repo.path().parent().unwrap();
|
|
let repo_path = root.as_os_str().to_str().unwrap();
|
|
|
|
File::create(&root.join(file_path))
|
|
.unwrap()
|
|
.write_all(b"test file1 content")
|
|
.unwrap();
|
|
|
|
File::create(&root.join(Path::new("file2.txt")))
|
|
.unwrap()
|
|
.write_all(b"test file2 content")
|
|
.unwrap();
|
|
|
|
assert_eq!(get_statuses(repo_path), (2, 0));
|
|
|
|
stage_add_file(repo_path, file_path).unwrap();
|
|
|
|
assert_eq!(get_statuses(repo_path), (1, 1));
|
|
}
|
|
|
|
#[test]
|
|
fn test_staging_folder() -> Result<()> {
|
|
let (_td, repo) = repo_init().unwrap();
|
|
let root = repo.path().parent().unwrap();
|
|
let repo_path = root.as_os_str().to_str().unwrap();
|
|
|
|
let status_count = |s: StatusType| -> usize {
|
|
get_status(repo_path, s, true).unwrap().len()
|
|
};
|
|
|
|
fs::create_dir_all(&root.join("a/d"))?;
|
|
File::create(&root.join(Path::new("a/d/f1.txt")))?
|
|
.write_all(b"foo")?;
|
|
File::create(&root.join(Path::new("a/d/f2.txt")))?
|
|
.write_all(b"foo")?;
|
|
File::create(&root.join(Path::new("a/f3.txt")))?
|
|
.write_all(b"foo")?;
|
|
|
|
assert_eq!(status_count(StatusType::WorkingDir), 3);
|
|
|
|
stage_add_all(repo_path, "a/d").unwrap();
|
|
|
|
assert_eq!(status_count(StatusType::WorkingDir), 1);
|
|
assert_eq!(status_count(StatusType::Stage), 2);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_staging_deleted_file() {
|
|
let file_path = Path::new("file1.txt");
|
|
let (_td, repo) = repo_init().unwrap();
|
|
let root = repo.path().parent().unwrap();
|
|
let repo_path = root.as_os_str().to_str().unwrap();
|
|
|
|
let status_count = |s: StatusType| -> usize {
|
|
get_status(repo_path, s, true).unwrap().len()
|
|
};
|
|
|
|
let full_path = &root.join(file_path);
|
|
|
|
File::create(full_path)
|
|
.unwrap()
|
|
.write_all(b"test file1 content")
|
|
.unwrap();
|
|
|
|
stage_add_file(repo_path, file_path).unwrap();
|
|
|
|
commit(repo_path, "commit msg").unwrap();
|
|
|
|
// delete the file now
|
|
assert_eq!(remove_file(full_path).is_ok(), true);
|
|
|
|
// deleted file in diff now
|
|
assert_eq!(status_count(StatusType::WorkingDir), 1);
|
|
|
|
stage_addremoved(repo_path, file_path).unwrap();
|
|
|
|
assert_eq!(status_count(StatusType::WorkingDir), 0);
|
|
assert_eq!(status_count(StatusType::Stage), 1);
|
|
}
|
|
|
|
// see https://github.com/extrawurst/gitui/issues/108
|
|
#[test]
|
|
fn test_staging_sub_git_folder() -> Result<()> {
|
|
let (_td, repo) = repo_init().unwrap();
|
|
let root = repo.path().parent().unwrap();
|
|
let repo_path = root.as_os_str().to_str().unwrap();
|
|
|
|
let status_count = |s: StatusType| -> usize {
|
|
get_status(repo_path, s, true).unwrap().len()
|
|
};
|
|
|
|
let sub = &root.join("sub");
|
|
|
|
fs::create_dir_all(sub)?;
|
|
|
|
debug_cmd_print(sub.to_str().unwrap(), "git init subgit");
|
|
|
|
File::create(sub.join("subgit/foo.txt"))
|
|
.unwrap()
|
|
.write_all(b"content")
|
|
.unwrap();
|
|
|
|
assert_eq!(status_count(StatusType::WorkingDir), 1);
|
|
|
|
//expect to fail
|
|
assert!(stage_add_all(repo_path, "sub").is_err());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_head_empty() -> Result<()> {
|
|
let (_td, repo) = repo_init_empty()?;
|
|
let root = repo.path().parent().unwrap();
|
|
let repo_path = root.as_os_str().to_str().unwrap();
|
|
|
|
assert_eq!(get_head(repo_path).is_ok(), false);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_head() -> Result<()> {
|
|
let (_td, repo) = repo_init()?;
|
|
let root = repo.path().parent().unwrap();
|
|
let repo_path = root.as_os_str().to_str().unwrap();
|
|
|
|
assert_eq!(get_head(repo_path).is_ok(), true);
|
|
|
|
Ok(())
|
|
}
|
|
}
|