use std::path::Path;
use std::process::Command;
use crate::output;
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
let work_dir = std::env::current_dir()?;
run_with_paths(&work_dir)
}
pub fn run_with_paths(work_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
let repo = match gix::discover(work_dir) {
Ok(r) => r,
Err(_) => {
return Err("not a git repository. \
Navigate to your project directory and run `entangle init` to set one up."
.into());
}
};
let has_origin = match repo.try_find_remote_without_url_rewrite("origin") {
None | Some(Err(_)) => false,
Some(Ok(_)) => true,
};
if !has_origin {
return Err("no 'origin' remote is configured. \
Run `entangle init` to set up the GitHub and Tangled push remotes."
.into());
}
if repo.head_id().is_err() {
return Err("no commits to push. \
Make your first commit, then run `entangle shove` again."
.into());
}
println!(
"{}",
output::progress("Pushing all branches to both forges…")
);
let branch_status = Command::new("git")
.args(["push", "origin", "--all"])
.current_dir(work_dir)
.status()?;
if !branch_status.success() {
let code = branch_status.code().unwrap_or(1);
return Err(format!(
"branch push failed (exit {code}). \
Check the output above for details, fix the issue, and re-run {}.",
output::cmd("entangle shove")
)
.into());
}
println!("{}", output::progress("Pushing tags to both forges…"));
let tag_status = Command::new("git")
.args(["push", "origin", "--tags"])
.current_dir(work_dir)
.status()?;
if !tag_status.success() {
let code = tag_status.code().unwrap_or(1);
return Err(format!(
"tag push failed (exit {code}). \
Check the output above for details, fix the issue, and re-run {}.",
output::cmd("entangle shove")
)
.into());
}
println!(
"{}",
output::success("All branches and tags pushed to both forges.")
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn shove_errors_on_non_git_directory() {
let dir = TempDir::new().unwrap();
let result = run_with_paths(dir.path());
assert!(result.is_err(), "shove must error in a non-git directory");
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("not a git repository"),
"error must mention 'not a git repository': {msg}"
);
assert!(
msg.contains("entangle init"),
"error must suggest entangle init: {msg}"
);
}
#[test]
fn shove_errors_when_no_origin_configured() {
let dir = TempDir::new().unwrap();
gix::init(dir.path()).unwrap();
let result = run_with_paths(dir.path());
assert!(
result.is_err(),
"shove must error when origin is not configured"
);
let msg = result.unwrap_err().to_string();
assert!(msg.contains("origin"), "error must mention 'origin': {msg}");
assert!(
msg.contains("entangle init"),
"error must suggest entangle init: {msg}"
);
}
#[test]
fn shove_errors_when_repo_has_no_commits() {
let dir = TempDir::new().unwrap();
gix::init(dir.path()).unwrap();
crate::git::create_origin_remote(
dir.path(),
"git@github.com:user/repo.git",
&[
"git@tangled.org:user.example.com/repo",
"git@github.com:user/repo.git",
],
)
.unwrap();
let result = run_with_paths(dir.path());
assert!(
result.is_err(),
"shove must error when there are no commits"
);
let msg = result.unwrap_err().to_string();
assert!(msg.contains("commit"), "error must mention 'commit': {msg}");
}
#[test]
fn shove_errors_when_only_upstream_remote_configured() {
let dir = TempDir::new().unwrap();
gix::init(dir.path()).unwrap();
use std::io::Write as _;
let config_path = dir.path().join(".git").join("config");
let mut file = std::fs::OpenOptions::new()
.append(true)
.open(&config_path)
.unwrap();
writeln!(file, "\n[remote \"upstream\"]").unwrap();
writeln!(file, "\turl = git@github.com:user/repo.git").unwrap();
writeln!(file, "\tfetch = +refs/heads/*:refs/remotes/upstream/*").unwrap();
let result = run_with_paths(dir.path());
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("origin"), "error must mention 'origin': {msg}");
}
}