use crate::gitopolis::GitopolisError;
use log::info;
use serde_derive::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Repos {
repos: Vec<Repo>,
}
#[derive(Debug)]
pub struct RepoInfo {
pub path: String,
pub tags: Vec<String>,
pub remotes: BTreeMap<String, Remote>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Repo {
pub path: String,
pub tags: Vec<String>,
pub remotes: BTreeMap<String, Remote>,
}
impl Repo {
fn new(path: String) -> Self {
Self {
path,
tags: vec![],
remotes: Default::default(),
}
}
pub(crate) fn add_remote(&mut self, name: String, url: String) {
self.remotes.insert(name.clone(), Remote { name, url });
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Remote {
pub name: String,
pub url: String,
}
impl Repos {
pub fn as_vec(&self) -> &Vec<Repo> {
&self.repos
}
pub fn into_vec(self) -> Vec<Repo> {
self.repos
}
pub fn new() -> Self {
Default::default()
}
pub fn new_with_repos(repos: Vec<Repo>) -> Self {
Repos { repos }
}
pub fn find_repo(&mut self, folder_name: String) -> Option<&mut Repo> {
if let Some(ix) = self.repo_index(folder_name) {
return Some(&mut self.repos[ix]);
}
None
}
pub fn repo_index(&self, folder_name: String) -> Option<usize> {
self.repos.iter().position(|r| r.path == *folder_name)
}
pub fn add(&mut self, repo_folder: String, remotes: BTreeMap<String, String>) {
let mut repo = Repo::new(repo_folder.clone());
for (name, url) in remotes {
repo.add_remote(name, url);
}
self.add_repo(repo);
}
pub fn add_with_tags_and_remotes(
&mut self,
repo_folder: String,
tags: Vec<String>,
remotes: BTreeMap<String, Remote>,
) {
let repo = Repo {
path: repo_folder.clone(),
tags,
remotes,
};
self.add_repo(repo);
}
fn add_repo(&mut self, repo: Repo) {
let repo_folder = repo.path.clone();
self.repos.push(repo);
self.repos
.sort_by(|a, b| a.path.to_lowercase().cmp(&b.path.to_lowercase()));
info!("Added {repo_folder}");
}
pub fn remove(&mut self, repo_folders: Vec<String>) {
for repo_folder in repo_folders {
match self.repo_index(repo_folder.to_owned()) {
Some(ix) => {
self.repos.remove(ix);
}
None => {
info!("Repo already absent, skipped: {repo_folder}")
}
}
}
}
pub fn add_tag(
&mut self,
tag_name: &str,
repo_folders: Vec<String>,
) -> Result<(), GitopolisError> {
self.tag(tag_name, repo_folders, false)
}
pub fn remove_tag(
&mut self,
tag_name: &str,
repo_folders: Vec<String>,
) -> Result<(), GitopolisError> {
self.tag(tag_name, repo_folders, true)
}
pub fn rename_tag(
&mut self,
old_name: &str,
new_name: &str,
merge: bool,
) -> Result<Vec<String>, GitopolisError> {
if !merge {
for repo in &self.repos {
if repo.tags.iter().any(|t| t == old_name)
&& repo.tags.iter().any(|t| t == new_name)
{
return Err(GitopolisError::StateError {
message: format!(
"Repo '{}' already has tag '{}'. Use --merge to combine.",
repo.path, new_name
),
});
}
}
}
let mut affected = Vec::new();
for repo in &mut self.repos {
if let Some(ix) = repo.tags.iter().position(|t| t == old_name) {
repo.tags.remove(ix);
if !repo.tags.iter().any(|t| t == new_name) {
repo.tags.push(new_name.to_string());
repo.tags.sort_by_key(|a| a.to_lowercase());
}
affected.push(repo.path.clone());
}
}
Ok(affected)
}
pub fn remove_tag_from_all(&mut self, tag_name: &str) -> Vec<String> {
let mut affected = Vec::new();
for repo in &mut self.repos {
if let Some(ix) = repo.tags.iter().position(|t| t == tag_name) {
repo.tags.remove(ix);
affected.push(repo.path.clone());
}
}
affected
}
fn tag(
&mut self,
tag_name: &str,
repo_folders: Vec<String>,
remove: bool,
) -> Result<(), GitopolisError> {
for repo_folder in repo_folders {
let repo = self.find_repo(repo_folder.to_owned()).ok_or_else(|| {
GitopolisError::StateError {
message: format!("Repo '{repo_folder}' not found"),
}
})?;
if remove {
if let Some(ix) = repo.tags.iter().position(|t| t == tag_name) {
repo.tags.remove(ix);
}
} else if !repo.tags.iter().any(|s| s == &tag_name.to_string()) {
repo.tags.push(tag_name.to_string());
repo.tags.sort_by_key(|a| a.to_lowercase());
}
}
Ok(())
}
}
#[test]
fn idempotent_tag() {
let mut repos = Repos::new();
let path = "repo_path".to_string();
let mut remotes = BTreeMap::new();
remotes.insert("origin".to_string(), "url".to_string());
repos.add(path.to_string(), remotes);
let tag = "tag_name";
repos
.add_tag(tag, vec![path.to_owned()])
.expect("add_tag failed");
repos
.add_tag(tag, vec![path.to_owned()])
.expect("add_tag failed");
let repo = repos.find_repo(path).expect("repo awol");
assert_eq!(1, repo.tags.len());
assert_eq!(tag, repo.tags[0]);
}