use std::path::Path;
use anyhow::{anyhow, bail, Context, Result};
use crate::commands::remote;
use crate::context::CommandContext;
use crate::style::Style;
const SETUP_FILE: &str = ".git-meta";
const DEFAULT_REMOTE_NAME: &str = "meta";
pub fn run() -> Result<()> {
let ctx = CommandContext::open(None)?;
let repo = ctx.session.repo();
let workdir = repo
.workdir()
.ok_or_else(|| anyhow!("git meta setup requires a non-bare repository"))?;
let setup_path = workdir.join(SETUP_FILE);
let url = read_setup_url(&setup_path)?;
let s = Style::detect_stderr();
eprintln!(
"{} metadata remote URL from {}",
s.step("Using"),
s.dim(&setup_path.display().to_string()),
);
remote::run_add(&url, DEFAULT_REMOTE_NAME, None, true)
}
fn read_setup_url(path: &Path) -> Result<String> {
if !path.exists() {
bail!(
"no {SETUP_FILE} file found at {display}\n\n\
Create one with the metadata remote URL on a single line, e.g.:\n \
echo 'git@github.com:org/project-meta.git' > {display}\n\n\
Or run `git meta remote add <url> --init` directly to skip the alias.",
display = path.display(),
);
}
let raw = std::fs::read_to_string(path)
.with_context(|| format!("read {SETUP_FILE} at {}", path.display()))?;
parse_setup_url(&raw)
.ok_or_else(|| {
anyhow!(
"{} is empty or contains no metadata remote URL\n\n\
Add a single non-comment line with the URL, for example:\n \
git@github.com:org/project-meta.git",
path.display(),
)
})
.map(str::to_string)
.map(strip_optional_trailing_slash_owned)
}
fn parse_setup_url(contents: &str) -> Option<&str> {
contents.lines().find_map(|line| {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
None
} else {
Some(trimmed)
}
})
}
fn strip_optional_trailing_slash_owned(url: String) -> String {
if url.len() > 1 && url.ends_with('/') {
let mut s = url;
s.pop();
s
} else {
url
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn parses_single_line_url() {
assert_eq!(
parse_setup_url("git@github.com:org/repo.git\n"),
Some("git@github.com:org/repo.git"),
);
}
#[test]
fn ignores_blank_and_comment_lines() {
let input = "\n\
# this is a comment\n\
\n\
# indented comment\n\
git@github.com:org/repo.git\n\
# trailing notes ignored\n";
assert_eq!(parse_setup_url(input), Some("git@github.com:org/repo.git"));
}
#[test]
fn returns_none_for_empty_input() {
assert_eq!(parse_setup_url(""), None);
}
#[test]
fn returns_none_for_only_comments() {
assert_eq!(parse_setup_url("# only a comment\n\n# another\n"), None);
}
#[test]
fn trims_surrounding_whitespace() {
assert_eq!(
parse_setup_url(" git@github.com:org/repo.git \n"),
Some("git@github.com:org/repo.git"),
);
}
#[test]
fn first_url_wins_when_multiple_lines() {
let input = "\
https://example.com/first.git\n\
https://example.com/second.git\n";
assert_eq!(
parse_setup_url(input),
Some("https://example.com/first.git")
);
}
#[test]
fn strips_single_trailing_slash() {
assert_eq!(
strip_optional_trailing_slash_owned("https://example.com/foo/".to_string()),
"https://example.com/foo"
);
}
#[test]
fn keeps_lone_slash() {
assert_eq!(strip_optional_trailing_slash_owned("/".to_string()), "/");
}
#[test]
fn read_setup_url_missing_file_errors_helpfully() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(".git-meta");
let err = read_setup_url(&path).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("no .git-meta file found"), "got: {msg}");
assert!(msg.contains("--init"), "got: {msg}");
}
#[test]
fn read_setup_url_empty_file_errors_helpfully() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(".git-meta");
std::fs::write(&path, "# only a comment\n\n").unwrap();
let err = read_setup_url(&path).unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("empty or contains no metadata remote URL"),
"got: {msg}"
);
}
}