gimmetool 0.1.3

A multi-repo manager that lets you jump between projects, pin favorites, clean stale branches, and alias repos — all from one CLI
use std::fs;
use std::path::Path;

use crate::config::{self, Config};
use crate::repo::{self, Repo};

pub struct SearchOptions {
  pub search_folders: Vec<String>,
  pub query: String,
}

pub fn for_repo(config: &Config, query: &str) -> SearchOptions {
  SearchOptions {
    search_folders: config::get_search_folders(config),
    query: query.to_string(),
  }
}

pub fn in_folders(folders: Vec<String>, query: &str) -> SearchOptions {
  SearchOptions {
    search_folders: folders,
    query: query.to_string(),
  }
}

pub fn default_options(config: &Config) -> SearchOptions {
  SearchOptions {
    search_folders: config::get_search_folders(config),
    query: String::new(),
  }
}

pub fn repositories(opts: &SearchOptions, config: &Config) -> Vec<Repo> {
  let pins = config::get_pinned_repos(config);

  let mut found: Vec<Repo> = opts
    .search_folders
    .iter()
    .flat_map(|folder| find_repos_recursively(folder, &opts.query, &pins))
    .collect();

  found.sort_by(|a, b| a.name.cmp(&b.name));
  found
}

pub fn sort_by_pins(repos: &mut [Repo]) {
  repos.sort_by(|a, b| match (a.pinned, b.pinned) {
    (true, false) => std::cmp::Ordering::Less,
    (false, true) => std::cmp::Ordering::Greater,
    (true, true) => a.pin_index.cmp(&b.pin_index),
    (false, false) => a.name.cmp(&b.name),
  });
}

pub fn find_repo_for_path(path: &str, config: &Config) -> Option<Repo> {
  let opts = default_options(config);
  let repos = repositories(&opts, config);
  repos.into_iter().find(|r| path.starts_with(&r.path))
}

fn find_repos_recursively(folder: &str, query: &str, pins: &[String]) -> Vec<Repo> {
  let entries = match fs::read_dir(folder) {
    Ok(e) => e,
    Err(e) => {
      eprintln!("ERROR Error reading directory \"{folder}\": {e}");
      return Vec::new();
    }
  };

  let mut results = Vec::new();

  for entry in entries.flatten() {
    let path = entry.path();

    if !path.is_dir() {
      continue;
    }

    let name = entry.file_name().to_string_lossy().to_string();
    let path_str = path.to_string_lossy().to_string();

    if is_git_repo(&path) {
      if name.contains(query) {
        let pin_index = pins.iter().position(|p| p == &path_str);
        let r = match pin_index {
          Some(idx) => repo::new_pinned(&path_str, &name, idx as i32),
          None => repo::new(&path_str, &name),
        };
        results.push(r);
      }
    } else {
      results.extend(find_repos_recursively(&path_str, query, pins));
    }
  }

  results
}

fn is_git_repo(path: &Path) -> bool {
  path.join(".git").exists()
}