use std::path::Path;
pub fn is_git_repo(path: &Path) -> bool {
gix::open(path).is_ok()
}
pub fn init_if_needed(path: &Path) -> Result<bool, Box<dyn std::error::Error>> {
if is_git_repo(path) {
return Ok(false);
}
gix::init(path)?;
Ok(true)
}
pub fn has_gitignore(path: &Path) -> bool {
path.join(".gitignore").exists()
}
pub fn has_readme(path: &Path) -> bool {
path.join("README.md").exists()
}
#[derive(Debug, Clone)]
pub enum OriginStatus {
Absent,
Present {
fetch_url: String,
push_urls: Vec<String>,
},
}
pub fn get_origin_status(work_dir: &Path) -> Result<OriginStatus, Box<dyn std::error::Error>> {
let repo = gix::open(work_dir)?;
let fetch_url = match repo.try_find_remote_without_url_rewrite("origin") {
None => return Ok(OriginStatus::Absent),
Some(Err(_)) => return Ok(OriginStatus::Absent),
Some(Ok(remote)) => match remote.url(gix::remote::Direction::Fetch) {
Some(url) => url.to_string(),
None => String::new(),
},
};
let push_urls = read_push_urls(work_dir, "origin");
Ok(OriginStatus::Present {
fetch_url,
push_urls,
})
}
pub fn create_origin_remote(
work_dir: &Path,
fetch_url: &str,
push_urls: &[&str],
) -> Result<(), Box<dyn std::error::Error>> {
use std::io::Write as _;
let config_path = work_dir.join(".git").join("config");
let mut file = std::fs::OpenOptions::new()
.append(true)
.open(&config_path)?;
writeln!(file, "\n[remote \"origin\"]")?;
writeln!(file, "\turl = {fetch_url}")?;
writeln!(file, "\tfetch = +refs/heads/*:refs/remotes/origin/*")?;
for url in push_urls {
writeln!(file, "\tpushurl = {url}")?;
}
Ok(())
}
pub fn set_origin_fetch_url(
work_dir: &Path,
new_url: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let config_path = work_dir.join(".git").join("config");
let content = std::fs::read_to_string(&config_path)?;
let modified = replace_url_in_origin_section(&content, new_url);
atomic_write(&config_path, &modified)?;
Ok(())
}
pub fn add_push_urls_to_origin(
work_dir: &Path,
push_urls: &[&str],
) -> Result<(), Box<dyn std::error::Error>> {
if push_urls.is_empty() {
return Ok(());
}
let config_path = work_dir.join(".git").join("config");
let content = std::fs::read_to_string(&config_path)?;
let modified = insert_push_urls_in_config(&content, push_urls);
atomic_write(&config_path, &modified)?;
Ok(())
}
fn section_header_matches(trimmed: &str, section: &str, subsection: &str) -> bool {
if !trimmed.starts_with('[') || !trimmed.ends_with(']') {
return false;
}
let inner = trimmed[1..trimmed.len() - 1].trim();
if let Some(quote_pos) = inner.find('"') {
let name = inner[..quote_pos].trim();
let rest = inner[quote_pos..].trim(); let expected_rest = format!("\"{subsection}\"");
name.eq_ignore_ascii_case(section) && rest == expected_rest
} else {
false
}
}
fn atomic_write(path: &std::path::Path, content: &str) -> Result<(), Box<dyn std::error::Error>> {
use std::io::Write as _;
let dir = path.parent().ok_or("path has no parent directory")?;
let mut tmp = tempfile::Builder::new().suffix(".lock").tempfile_in(dir)?;
tmp.write_all(content.as_bytes())?;
tmp.persist(path)?;
Ok(())
}
fn replace_url_in_origin_section(config_text: &str, new_url: &str) -> String {
let mut in_section = false;
let mut replaced = false;
let mut result = String::with_capacity(config_text.len() + new_url.len());
for line in config_text.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
in_section = section_header_matches(trimmed, "remote", "origin");
result.push_str(line);
result.push('\n');
continue;
}
if in_section
&& !replaced
&& let Some(eq_pos) = trimmed.find('=')
{
let key = trimmed[..eq_pos].trim();
if key.eq_ignore_ascii_case("url") {
let indent_len = line.len() - line.trim_start().len();
let indent = &line[..indent_len];
result.push_str(indent);
result.push_str("url = ");
result.push_str(new_url);
result.push('\n');
replaced = true;
continue;
}
}
result.push_str(line);
result.push('\n');
}
result
}
fn insert_push_urls_in_config(config_text: &str, push_urls: &[&str]) -> String {
if push_urls.is_empty() {
return config_text.to_string();
}
let mut in_section = false;
let mut inserted = false;
let mut result = String::with_capacity(config_text.len() + push_urls.len() * 60);
for line in config_text.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
if in_section && !inserted {
for url in push_urls {
result.push_str("\tpushurl = ");
result.push_str(url);
result.push('\n');
}
inserted = true;
}
in_section = section_header_matches(trimmed, "remote", "origin");
}
result.push_str(line);
result.push('\n');
}
if in_section && !inserted {
for url in push_urls {
result.push_str("\tpushurl = ");
result.push_str(url);
result.push('\n');
}
}
result
}
fn read_push_urls(work_dir: &Path, remote_name: &str) -> Vec<String> {
let config_path = work_dir.join(".git").join("config");
let content = match std::fs::read_to_string(&config_path) {
Ok(c) => c,
Err(_) => return vec![],
};
let mut in_section = false;
let mut push_urls = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
continue;
}
if trimmed.starts_with('[') {
in_section = section_header_matches(trimmed, "remote", remote_name);
continue;
}
if in_section && let Some(eq_pos) = trimmed.find('=') {
let key = trimmed[..eq_pos].trim();
if key.eq_ignore_ascii_case("pushurl") {
let value = trimmed[eq_pos + 1..].trim();
push_urls.push(value.to_string());
}
}
}
push_urls
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn is_git_repo_returns_false_for_empty_directory() {
let dir = TempDir::new().unwrap();
assert!(
!is_git_repo(dir.path()),
"empty directory must not be a git repo"
);
}
#[test]
fn is_git_repo_returns_false_for_directory_with_files_but_no_git() {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join("main.rs"),
b"fn main() { println!(\"hi\"); }",
)
.unwrap();
assert!(
!is_git_repo(dir.path()),
"directory with files but no .git must not be a git repo"
);
}
#[test]
fn is_git_repo_returns_true_after_gix_init() {
let dir = TempDir::new().unwrap();
gix::init(dir.path()).expect("gix::init must succeed on a fresh temp dir");
assert!(
is_git_repo(dir.path()),
"directory with .git must be a git repo"
);
}
#[test]
fn is_git_repo_returns_false_for_subdirectory_of_repo() {
let dir = TempDir::new().unwrap();
gix::init(dir.path()).unwrap();
let sub = dir.path().join("src");
std::fs::create_dir(&sub).unwrap();
assert!(
!is_git_repo(&sub),
"subdirectory of a git repo must not itself be detected as a repo root"
);
}
#[test]
fn init_if_needed_creates_git_repo_and_returns_true() {
let dir = TempDir::new().unwrap();
let was_new = init_if_needed(dir.path()).expect("init_if_needed must succeed");
assert!(
was_new,
"must return true when initializing a fresh directory"
);
assert!(
is_git_repo(dir.path()),
"directory must be a git repo after init"
);
}
#[test]
fn init_if_needed_is_idempotent_and_returns_false() {
let dir = TempDir::new().unwrap();
init_if_needed(dir.path()).unwrap();
let was_new = init_if_needed(dir.path()).expect("second call must not error");
assert!(
!was_new,
"must return false for an already-initialized repo"
);
}
#[test]
fn init_if_needed_does_not_overwrite_existing_repo_files() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("my_file.txt"), b"hello").unwrap();
init_if_needed(dir.path()).unwrap();
assert!(
dir.path().join("my_file.txt").exists(),
"init must not destroy pre-existing files in the directory"
);
}
#[test]
fn has_gitignore_returns_false_when_absent() {
let dir = TempDir::new().unwrap();
assert!(!has_gitignore(dir.path()));
}
#[test]
fn has_gitignore_returns_true_when_present() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join(".gitignore"), b"target/\n").unwrap();
assert!(has_gitignore(dir.path()));
}
#[test]
fn has_gitignore_does_not_match_differently_named_files() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("gitignore"), b"target/\n").unwrap(); assert!(
!has_gitignore(dir.path()),
"file named 'gitignore' (no leading dot) must not match"
);
}
#[test]
fn has_readme_returns_false_when_absent() {
let dir = TempDir::new().unwrap();
assert!(!has_readme(dir.path()));
}
#[test]
fn has_readme_returns_true_when_readme_md_present() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("README.md"), b"# My Project\n").unwrap();
assert!(has_readme(dir.path()));
}
#[test]
fn has_readme_does_not_match_readme_txt() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("README.txt"), b"My Project\n").unwrap();
assert!(
!has_readme(dir.path()),
"README.txt must not match — we only check for README.md"
);
}
fn append_origin_remote(work_dir: &Path, fetch_url: &str, push_urls: &[&str]) {
use std::io::Write as _;
let config_path = work_dir.join(".git").join("config");
let mut file = std::fs::OpenOptions::new()
.append(true)
.open(&config_path)
.expect("must open .git/config");
writeln!(file, "\n[remote \"origin\"]").unwrap();
writeln!(file, "\turl = {fetch_url}").unwrap();
writeln!(file, "\tfetch = +refs/heads/*:refs/remotes/origin/*").unwrap();
for url in push_urls {
writeln!(file, "\tpushurl = {url}").unwrap();
}
}
#[test]
fn get_origin_status_absent_for_fresh_repo() {
let dir = TempDir::new().unwrap();
gix::init(dir.path()).unwrap();
let status = get_origin_status(dir.path()).expect("must not error");
assert!(
matches!(status, OriginStatus::Absent),
"fresh repo with no remotes must return Absent"
);
}
#[test]
fn get_origin_status_present_with_no_push_urls() {
let dir = TempDir::new().unwrap();
gix::init(dir.path()).unwrap();
append_origin_remote(dir.path(), "git@github.com:cyrusae/entangle.git", &[]);
let status = get_origin_status(dir.path()).expect("must not error");
match status {
OriginStatus::Present {
fetch_url,
push_urls,
} => {
assert_eq!(fetch_url, "git@github.com:cyrusae/entangle.git");
assert!(
push_urls.is_empty(),
"no pushurl entries means empty push_urls vec"
);
}
OriginStatus::Absent => panic!("expected Present, got Absent"),
}
}
#[test]
fn get_origin_status_present_with_one_push_url() {
let dir = TempDir::new().unwrap();
gix::init(dir.path()).unwrap();
append_origin_remote(
dir.path(),
"git@github.com:cyrusae/entangle.git",
&["git@github.com:cyrusae/entangle.git"],
);
let status = get_origin_status(dir.path()).expect("must not error");
match status {
OriginStatus::Present { push_urls, .. } => {
assert_eq!(push_urls.len(), 1);
assert_eq!(push_urls[0], "git@github.com:cyrusae/entangle.git");
}
OriginStatus::Absent => panic!("expected Present, got Absent"),
}
}
#[test]
fn get_origin_status_present_with_two_push_urls() {
let dir = TempDir::new().unwrap();
gix::init(dir.path()).unwrap();
append_origin_remote(
dir.path(),
"git@github.com:cyrusae/entangle.git",
&[
"git@github.com:cyrusae/entangle.git",
"git@tangled.org:atdot.fyi/entangle",
],
);
let status = get_origin_status(dir.path()).expect("must not error");
match status {
OriginStatus::Present { push_urls, .. } => {
assert_eq!(push_urls.len(), 2, "must return both push URLs");
assert!(push_urls.contains(&"git@github.com:cyrusae/entangle.git".to_string()));
assert!(push_urls.contains(&"git@tangled.org:atdot.fyi/entangle".to_string()));
}
OriginStatus::Absent => panic!("expected Present, got Absent"),
}
}
#[test]
fn get_origin_status_absent_when_different_remote_name_present() {
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:someone/repo.git").unwrap();
writeln!(file, "\tfetch = +refs/heads/*:refs/remotes/upstream/*").unwrap();
let status = get_origin_status(dir.path()).expect("must not error");
assert!(
matches!(status, OriginStatus::Absent),
"a remote named 'upstream' must not be detected as 'origin'"
);
}
#[test]
fn read_push_urls_returns_empty_for_repo_with_no_pushurl() {
let dir = TempDir::new().unwrap();
gix::init(dir.path()).unwrap();
append_origin_remote(dir.path(), "git@github.com:cyrusae/entangle.git", &[]);
let urls = read_push_urls(dir.path(), "origin");
assert!(urls.is_empty(), "no pushurl entries should yield empty vec");
}
#[test]
fn section_header_matches_standard_lowercase() {
assert!(section_header_matches(
"[remote \"origin\"]",
"remote",
"origin"
));
}
#[test]
fn section_header_matches_uppercase_section_name() {
assert!(section_header_matches(
"[Remote \"origin\"]",
"remote",
"origin"
));
}
#[test]
fn section_header_matches_allcaps_section_name() {
assert!(section_header_matches(
"[REMOTE \"origin\"]",
"remote",
"origin"
));
}
#[test]
fn section_header_matches_subsection_is_case_sensitive() {
assert!(!section_header_matches(
"[remote \"Origin\"]",
"remote",
"origin"
));
assert!(!section_header_matches(
"[remote \"ORIGIN\"]",
"remote",
"origin"
));
}
#[test]
fn section_header_matches_different_remote_name() {
assert!(!section_header_matches(
"[remote \"upstream\"]",
"remote",
"origin"
));
}
#[test]
fn section_header_matches_different_section() {
assert!(!section_header_matches(
"[branch \"main\"]",
"remote",
"origin"
));
}
#[test]
fn section_header_matches_no_subsection() {
assert!(!section_header_matches("[core]", "remote", "origin"));
}
#[test]
fn read_push_urls_handles_case_insensitive_section_name() {
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 \"origin\"]").unwrap();
writeln!(file, "\turl = git@github.com:cyrusae/entangle.git").unwrap();
writeln!(file, "\tpushurl = git@tangled.org:atdot.fyi/entangle").unwrap();
let urls = read_push_urls(dir.path(), "origin");
assert_eq!(
urls,
vec!["git@tangled.org:atdot.fyi/entangle"],
"case-insensitive section name must be recognized"
);
}
#[test]
fn read_push_urls_handles_case_insensitive_key() {
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 \"origin\"]").unwrap();
writeln!(file, "\turl = git@github.com:cyrusae/entangle.git").unwrap();
writeln!(file, "\tPushUrl = git@tangled.org:atdot.fyi/entangle").unwrap();
let urls = read_push_urls(dir.path(), "origin");
assert_eq!(urls, vec!["git@tangled.org:atdot.fyi/entangle"]);
}
#[test]
fn replace_url_in_origin_section_replaces_url() {
let config = "[core]\n\trepositoryformatversion = 0\n\n[remote \"origin\"]\n\turl = git@github.com:old/repo.git\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n";
let result = replace_url_in_origin_section(config, "git@github.com:new/repo.git");
assert!(
result.contains("\turl = git@github.com:new/repo.git"),
"must contain the new url: {result}"
);
assert!(
!result.contains("url = git@github.com:old/repo.git"),
"must not contain old url: {result}"
);
assert!(result.contains("[core]"), "must preserve [core] section");
}
#[test]
fn replace_url_in_origin_section_preserves_indentation() {
let config = "[remote \"origin\"]\n\t\turl = old\n";
let result = replace_url_in_origin_section(config, "new");
assert!(
result.contains("\t\turl = new"),
"must preserve original indentation: {result}"
);
}
#[test]
fn replace_url_in_origin_section_does_not_touch_other_remotes() {
let config =
"[remote \"upstream\"]\n\turl = upstream_url\n[remote \"origin\"]\n\turl = old_url\n";
let result = replace_url_in_origin_section(config, "new_url");
assert!(
result.contains("upstream_url"),
"must not modify upstream remote"
);
assert!(result.contains("url = new_url"), "must update origin url");
assert!(!result.contains("url = old_url"), "old url must be gone");
}
#[test]
fn replace_url_in_origin_section_returns_unchanged_when_section_absent() {
let config = "[core]\n\trepositoryformatversion = 0\n";
let result = replace_url_in_origin_section(config, "some_url");
assert!(
!result.contains("some_url"),
"must not insert url when section is absent: {result}"
);
assert!(
result.contains("[core]"),
"core section must still be present"
);
}
#[test]
fn insert_push_urls_in_config_inserts_before_next_section() {
let config =
"[remote \"origin\"]\n\turl = origin_url\n[branch \"main\"]\n\tremote = origin\n";
let result = insert_push_urls_in_config(config, &["push_url_1", "push_url_2"]);
let push1_pos = result
.find("pushurl = push_url_1")
.expect("push_url_1 must be in result");
let branch_pos = result
.find("[branch")
.expect("[branch] must still be in result");
assert!(
push1_pos < branch_pos,
"pushurls must appear before [branch] header"
);
assert!(
result.contains("pushurl = push_url_2"),
"push_url_2 must be present"
);
}
#[test]
fn insert_push_urls_in_config_appends_when_last_section() {
let config = "[remote \"origin\"]\n\turl = origin_url\n\tfetch = +refs/heads/*\n";
let result = insert_push_urls_in_config(config, &["push_url_1"]);
assert!(
result.contains("\tpushurl = push_url_1"),
"must add pushurl at end"
);
}
#[test]
fn insert_push_urls_in_config_uses_tab_indent() {
let config = "[remote \"origin\"]\n\turl = url\n";
let result = insert_push_urls_in_config(config, &["my_url"]);
assert!(
result.contains("\tpushurl = my_url"),
"pushurl must be tab-indented: {result}"
);
}
#[test]
fn insert_push_urls_in_config_returns_unchanged_for_empty_slice() {
let config = "[remote \"origin\"]\n\turl = url\n";
let result = insert_push_urls_in_config(config, &[]);
assert_eq!(
result, config,
"empty push_urls must return unchanged string"
);
}
#[test]
fn insert_push_urls_in_config_returns_unchanged_when_section_absent() {
let config = "[core]\n\trepositoryformatversion = 0\n";
let result = insert_push_urls_in_config(config, &["some_url"]);
assert!(
!result.contains("pushurl"),
"must not insert pushurl when section is absent"
);
}
#[test]
fn create_origin_remote_creates_section_with_fetch_and_push_urls() {
let dir = TempDir::new().unwrap();
gix::init(dir.path()).unwrap();
create_origin_remote(
dir.path(),
"git@github.com:user/repo.git",
&["git@github.com:user/repo.git", "git@tangled.org:user/repo"],
)
.unwrap();
let status = get_origin_status(dir.path()).unwrap();
match status {
OriginStatus::Present {
fetch_url,
push_urls,
} => {
assert_eq!(fetch_url, "git@github.com:user/repo.git");
assert_eq!(push_urls.len(), 2, "must have both push URLs");
assert!(push_urls.contains(&"git@github.com:user/repo.git".to_string()));
assert!(push_urls.contains(&"git@tangled.org:user/repo".to_string()));
}
OriginStatus::Absent => panic!("expected Present after create_origin_remote"),
}
}
#[test]
fn create_origin_remote_with_no_push_urls_creates_section() {
let dir = TempDir::new().unwrap();
gix::init(dir.path()).unwrap();
create_origin_remote(dir.path(), "git@github.com:user/repo.git", &[]).unwrap();
let status = get_origin_status(dir.path()).unwrap();
match status {
OriginStatus::Present {
fetch_url,
push_urls,
} => {
assert_eq!(fetch_url, "git@github.com:user/repo.git");
assert!(push_urls.is_empty(), "no push URLs should be configured");
}
OriginStatus::Absent => panic!("expected Present"),
}
}
#[test]
fn set_origin_fetch_url_replaces_existing_url() {
let dir = TempDir::new().unwrap();
gix::init(dir.path()).unwrap();
append_origin_remote(dir.path(), "git@github.com:old/repo.git", &[]);
set_origin_fetch_url(dir.path(), "git@github.com:new/repo.git").unwrap();
let status = get_origin_status(dir.path()).unwrap();
match status {
OriginStatus::Present { fetch_url, .. } => {
assert_eq!(
fetch_url, "git@github.com:new/repo.git",
"fetch URL must be updated"
);
}
OriginStatus::Absent => panic!("expected Present"),
}
}
#[test]
fn set_origin_fetch_url_preserves_push_urls() {
let dir = TempDir::new().unwrap();
gix::init(dir.path()).unwrap();
append_origin_remote(
dir.path(),
"git@github.com:old/repo.git",
&["git@github.com:old/repo.git"],
);
set_origin_fetch_url(dir.path(), "git@github.com:new/repo.git").unwrap();
let status = get_origin_status(dir.path()).unwrap();
match status {
OriginStatus::Present {
fetch_url,
push_urls,
} => {
assert_eq!(fetch_url, "git@github.com:new/repo.git");
assert_eq!(push_urls, vec!["git@github.com:old/repo.git"]);
}
OriginStatus::Absent => panic!("expected Present"),
}
}
#[test]
fn add_push_urls_to_origin_appends_to_existing_remote() {
let dir = TempDir::new().unwrap();
gix::init(dir.path()).unwrap();
append_origin_remote(dir.path(), "git@github.com:user/repo.git", &[]);
add_push_urls_to_origin(
dir.path(),
&["git@github.com:user/repo.git", "git@tangled.org:user/repo"],
)
.unwrap();
let urls = read_push_urls(dir.path(), "origin");
assert_eq!(urls.len(), 2, "must have both push URLs");
assert!(urls.contains(&"git@github.com:user/repo.git".to_string()));
assert!(urls.contains(&"git@tangled.org:user/repo".to_string()));
}
#[test]
fn add_push_urls_to_origin_is_noop_for_empty_slice() {
let dir = TempDir::new().unwrap();
gix::init(dir.path()).unwrap();
append_origin_remote(dir.path(), "git@github.com:user/repo.git", &[]);
add_push_urls_to_origin(dir.path(), &[]).unwrap();
let urls = read_push_urls(dir.path(), "origin");
assert!(
urls.is_empty(),
"no push URLs should be added from an empty slice"
);
}
#[test]
fn add_push_urls_to_origin_preserves_fetch_url() {
let dir = TempDir::new().unwrap();
gix::init(dir.path()).unwrap();
append_origin_remote(dir.path(), "git@github.com:user/repo.git", &[]);
add_push_urls_to_origin(dir.path(), &["git@tangled.org:user/repo"]).unwrap();
let status = get_origin_status(dir.path()).unwrap();
match status {
OriginStatus::Present { fetch_url, .. } => {
assert_eq!(
fetch_url, "git@github.com:user/repo.git",
"fetch URL must be preserved"
);
}
OriginStatus::Absent => panic!("expected Present"),
}
}
#[test]
fn read_push_urls_collects_across_multiple_sections() {
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 \"origin\"]").unwrap();
writeln!(file, "\turl = git@github.com:cyrusae/entangle.git").unwrap();
writeln!(file, "\tpushurl = git@github.com:cyrusae/entangle.git").unwrap();
writeln!(file, "\n[remote \"origin\"]").unwrap();
writeln!(file, "\tpushurl = git@tangled.org:atdot.fyi/entangle").unwrap();
let urls = read_push_urls(dir.path(), "origin");
assert_eq!(
urls.len(),
2,
"both pushurl entries from repeated sections must be collected"
);
assert!(urls.contains(&"git@github.com:cyrusae/entangle.git".to_string()));
assert!(urls.contains(&"git@tangled.org:atdot.fyi/entangle".to_string()));
}
}