use anyhow::{Context, Error};
use std::process::Command;
use crate::git::repository::core::{GitRepo, RemoteInfo};
impl GitRepo {
pub fn add_remote(&self, name: &str, url: &str) -> Result<(), Error> {
self.repo()
.remote(name, url)
.context(format!("Failed to add remote '{name}' with URL '{url}'"))?;
Ok(())
}
pub fn set_remote_url(&self, name: &str, url: &str) -> Result<(), Error> {
self.repo()
.remote_set_url(name, url)
.context(format!("Failed to set URL for remote '{name}'"))?;
Ok(())
}
pub fn get_remotes(&self) -> Result<Vec<RemoteInfo>, Error> {
let remotes = self
.repo()
.remotes()
.context("Failed to get remotes list")?;
let mut remote_infos = Vec::new();
for i in 0..remotes.len() {
if let Some(name) = remotes.get(i) {
let remote = self
.repo()
.find_remote(name)
.context(format!("Failed to find remote '{name}'"))?;
let url = remote.url().unwrap_or("<no url>").to_string();
remote_infos.push(RemoteInfo {
name: name.to_string(),
url,
});
}
}
Ok(remote_infos)
}
pub fn get_remote_names(&self) -> Result<Vec<String>, Error> {
let remotes = self.get_remotes()?;
Ok(remotes.into_iter().map(|r| r.name).collect())
}
pub fn get_remote_url(&self, name: &str) -> Result<String, Error> {
let remote = self
.repo()
.find_remote(name)
.context(format!("Failed to find remote '{name}'"))?;
let url = remote
.url()
.ok_or_else(|| anyhow::anyhow!("Remote '{name}' has no URL"))?;
Ok(url.to_string())
}
pub fn push(&self, remote_name: &str, branch_name: &str) -> Result<(), Error> {
let mut remote = self
.repo()
.find_remote(remote_name)
.context(format!("Failed to find remote '{remote_name}'"))?;
let refspec = format!("refs/heads/{branch_name}:refs/heads/{branch_name}");
remote.push(&[&refspec], None).context(format!(
"Failed to push branch '{branch_name}' to remote '{remote_name}'"
))?;
Ok(())
}
pub fn push_current_branch(&self, remote_name: &str) -> Result<(), Error> {
let head_target = self
.get_head_symbolic_target()
.context("Failed to get current branch from HEAD")?;
let branch_name = head_target
.strip_prefix("refs/heads/")
.ok_or_else(|| anyhow::anyhow!("HEAD is not pointing to a branch"))?;
self.push(remote_name, branch_name)
}
pub fn push_to_origin(&self) -> Result<(), Error> {
self.push_current_branch("origin")
}
pub fn force_push_commit_to_branch(
&self,
remote_name: &str,
commit_sha: &str,
branch_name: &str,
) -> Result<(), Error> {
let status = Command::new("git")
.arg("push")
.arg("--force-with-lease")
.arg(remote_name)
.arg(format!("{commit_sha}:refs/heads/{branch_name}"))
.current_dir(self.path())
.status()
.context("Failed to execute git push")?;
if !status.success() {
return Err(anyhow::anyhow!(
"Failed to force-push commit '{commit_sha}' to '{remote_name}/{branch_name}'"
));
}
Ok(())
}
pub fn push_commit_to_branch(
&self,
remote_name: &str,
commit_sha: &str,
branch_name: &str,
) -> Result<(), Error> {
let status = Command::new("git")
.arg("push")
.arg(remote_name)
.arg(format!("{commit_sha}:refs/heads/{branch_name}"))
.current_dir(self.path())
.status()
.context("Failed to execute git push")?;
if !status.success() {
return Err(anyhow::anyhow!(
"Failed to push commit '{commit_sha}' to '{remote_name}/{branch_name}'"
));
}
Ok(())
}
pub fn delete_remote_branch(&self, remote_name: &str, branch_name: &str) -> Result<(), Error> {
let status = Command::new("git")
.arg("push")
.arg(remote_name)
.arg(format!(":refs/heads/{branch_name}"))
.current_dir(self.path())
.status()
.context("Failed to execute git push for remote branch delete")?;
if !status.success() {
return Err(anyhow::anyhow!(
"Failed to delete remote branch '{remote_name}/{branch_name}'"
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::{
git::{repository::core::RemoteInfo, GitRepo},
test_utils::{create_test_bare_repo, create_test_repo, RepoTestOperations},
};
#[test]
fn add_remote_works() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let path = temp_dir.path();
let repo = GitRepo::init(path).unwrap();
let remotes = repo.get_remotes().unwrap();
assert_eq!(remotes.len(), 0);
repo.add_remote("origin", "https://url1").unwrap();
let remotes = repo.get_remotes().unwrap();
assert_eq!(
remotes,
vec![RemoteInfo {
name: "origin".to_string(),
url: "https://url1".to_string()
}]
);
}
#[test]
fn set_remote_url_works() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let path = temp_dir.path();
let repo = GitRepo::init(path).unwrap();
let remotes = repo.get_remotes().unwrap();
assert_eq!(remotes.len(), 0);
repo.add_remote("origin", "https://url1").unwrap();
let remotes = repo.get_remotes().unwrap();
assert_eq!(
remotes,
vec![RemoteInfo {
name: "origin".to_string(),
url: "https://url1".to_string()
}]
);
repo.set_remote_url("origin", "https://url2").unwrap();
let remotes = repo.get_remotes().unwrap();
assert_eq!(
remotes,
vec![RemoteInfo {
name: "origin".to_string(),
url: "https://url2".to_string()
}]
);
}
#[test]
fn get_remotes_works() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let path = temp_dir.path();
let repo = GitRepo::init(path).unwrap();
let remotes = repo.get_remotes().unwrap();
assert_eq!(remotes.len(), 0);
repo.add_remote("origin", "https://url1").unwrap();
repo.add_remote("origin_2", "https://url2").unwrap();
let remotes = repo.get_remotes().unwrap();
assert_eq!(
remotes,
vec![
RemoteInfo {
name: "origin".to_string(),
url: "https://url1".to_string()
},
RemoteInfo {
name: "origin_2".to_string(),
url: "https://url2".to_string()
}
]
);
}
#[test]
fn get_remote_names_works() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let path = temp_dir.path();
let repo = GitRepo::init(path).unwrap();
let remote_names = repo.get_remote_names().unwrap();
assert_eq!(remote_names.len(), 0);
repo.add_remote("origin", "https://url1").unwrap();
repo.add_remote("origin_2", "https://url2").unwrap();
let remote_names = repo.get_remote_names().unwrap();
assert_eq!(remote_names, vec!["origin", "origin_2"]);
}
#[test]
fn push_works() {
let (_remote_dir, remote_repo) = create_test_bare_repo();
let remote_branches = remote_repo.get_all_branches().unwrap();
assert_eq!(remote_branches.len(), 0);
let (_local_dir, local_repo) = create_test_repo();
local_repo
.add_file_and_commit("test.txt", "content", "Initial commit")
.unwrap();
local_repo.add_local_remote("origin", &remote_repo).unwrap();
local_repo.push("origin", "master").unwrap();
let remote_branches = remote_repo.get_all_branches().unwrap();
assert_eq!(remote_branches, vec!["master"]);
}
#[test]
fn push_current_branch_works() {
let (_remote_dir, remote_repo) = create_test_bare_repo();
let (_local_dir, local_repo) = create_test_repo();
local_repo
.add_file_and_commit("test.txt", "content", "Initial commit")
.unwrap();
local_repo
.create_and_checkout_branch("feature_branch")
.unwrap();
local_repo
.add_file_and_commit("feature.txt", "feature content", "Feature commit")
.unwrap();
local_repo.add_local_remote("origin", &remote_repo).unwrap();
local_repo.push_current_branch("origin").unwrap();
let remote_branches = remote_repo.get_all_branches().unwrap();
assert_eq!(remote_branches, vec!["feature_branch"]);
}
#[test]
fn push_to_origin_works() {
let (_remote_dir, remote_repo) = create_test_bare_repo();
let (_local_dir, local_repo) = create_test_repo();
local_repo
.add_file_and_commit("test.txt", "content", "Initial commit")
.unwrap();
local_repo
.create_and_checkout_branch("feature_branch")
.unwrap();
local_repo
.add_file_and_commit("feature.txt", "feature content", "Feature commit")
.unwrap();
local_repo.add_local_remote("origin", &remote_repo).unwrap();
local_repo.push_to_origin().unwrap();
let remote_branches = remote_repo.get_all_branches().unwrap();
assert_eq!(remote_branches, vec!["feature_branch"]);
}
}