quinjet 0.0.49

A fast, live, keyboard-first Git source-control interface for the terminal
#[cfg_attr(not(test), expect(clippy::wildcard_imports, reason = "shared"))]
use super::*;

impl Repository {
    pub(crate) fn github_repositories(
        &self,
        refresh: bool,
    ) -> Result<(Vec<GitHubRepository>, Vec<String>)> {
        let (remote_urls, mut warnings) = self.remote_urls()?;
        let grouped_remote_urls = group_remote_urls(&remote_urls);
        let mut repositories = BTreeMap::new();

        for (index, (url, remotes)) in grouped_remote_urls.iter().take(MAX_REMOTE_URLS).enumerate()
        {
            match repository_from_remote_url(url)
                .map(|repository| (repository, CacheDisposition::Fresh))
                .map_or_else(|| self.resolve_github_repository(Some(url), refresh), Ok)
            {
                Ok((repository, disposition)) => {
                    if disposition == CacheDisposition::Stale {
                        warnings.push(format!(
                            "Using stale cached GitHub identity for remote{} {}",
                            if remotes.len() == 1 { "" } else { "s" },
                            remotes.join(", ")
                        ));
                    }
                    if remotes.is_empty() {
                        merge_repository(&mut repositories, repository, None);
                    } else {
                        for remote in remotes {
                            merge_repository(&mut repositories, repository.clone(), Some(remote));
                        }
                    }
                }
                Err(error) => warnings.push(format!(
                    "remote{} `{}` {} not available through gh: {error}",
                    if remotes.len() == 1 { "" } else { "s" },
                    remotes.join(", "),
                    if remotes.len() == 1 { "is" } else { "are" }
                )),
            }
            if repositories.len() >= MAX_GITHUB_REPOSITORIES {
                if index + 1 < grouped_remote_urls.len().min(MAX_REMOTE_URLS) {
                    warnings.push(format!(
                        "Only the first {MAX_GITHUB_REPOSITORIES} distinct GitHub repositories were loaded"
                    ));
                }
                break;
            }
        }
        if grouped_remote_urls.len() > MAX_REMOTE_URLS {
            warnings.push(format!(
                "Only the first {MAX_REMOTE_URLS} distinct fetch/push remote URLs were inspected"
            ));
        }

        if repositories.is_empty() {
            match self.resolve_github_repository(None, refresh) {
                Ok((repository, disposition)) => {
                    if disposition == CacheDisposition::Stale {
                        warnings.push("Using a stale cached inferred GitHub repository".to_owned());
                    }
                    merge_repository(&mut repositories, repository, None);
                }
                Err(error) => {
                    let remote_hint = if remote_urls.is_empty() {
                        "No Git remotes are configured".to_owned()
                    } else {
                        "No configured fetch or push remote resolves to GitHub".to_owned()
                    };
                    bail!("{remote_hint}; GitHub CLI could not infer a repository: {error}");
                }
            }
        }

        let mut repositories: Vec<_> = repositories.into_values().collect();
        for repository in &mut repositories {
            repository.remotes.sort();
            repository.remotes.dedup();
        }
        repositories.sort_by_key(|repository| {
            (
                !repository.remotes.iter().any(|remote| remote == "origin"),
                repository.display_name().to_lowercase(),
            )
        });
        Ok((repositories, warnings))
    }

    pub(crate) fn local_github_repository(&self) -> Result<Option<GitHubRepository>> {
        let (remote_urls, _) = self.remote_urls()?;
        let mut repositories = BTreeMap::new();
        for remote_url in remote_urls {
            if let Some(repository) = repository_from_remote_url(&remote_url.url) {
                merge_repository(&mut repositories, repository, Some(&remote_url.remote));
            }
        }
        let mut repositories = repositories.into_values().collect::<Vec<_>>();
        repositories.sort_by_key(|repository| {
            (
                !repository.remotes.iter().any(|remote| remote == "origin"),
                repository.display_name().to_lowercase(),
            )
        });
        Ok(repositories.into_iter().next())
    }

    pub(super) fn remote_urls(&self) -> Result<(Vec<RemoteUrl>, Vec<String>)> {
        let output = self.checked([OsString::from("remote")])?;
        let mut urls = BTreeSet::new();
        let mut warnings = Vec::new();

        let mut remote_count = 0;
        'remotes: for record in output.split(|byte| *byte == b'\n') {
            let record = trim_ascii(record);
            if record.is_empty() {
                continue;
            }
            if remote_count >= MAX_GIT_REMOTES {
                warnings.push(format!(
                    "Only the first {MAX_GIT_REMOTES} Git remotes were inspected"
                ));
                break;
            }
            remote_count += 1;
            let remote = text(record);
            for push in [false, true] {
                let mut args = vec![OsString::from("remote"), OsString::from("get-url")];
                if push {
                    args.push(OsString::from("--push"));
                }
                args.push(OsString::from("--all"));
                args.push(OsString::from(&remote));
                let remote_output = self.run(args)?;
                if !remote_output.status.success() {
                    if !push {
                        warnings.push(format!("Unable to read URL for remote `{remote}`"));
                    }
                    continue;
                }
                for url in remote_output.stdout.split(|byte| *byte == b'\n') {
                    let url = trim_ascii(url);
                    if url.is_empty() {
                        continue;
                    }
                    let entry = (remote.clone(), text(url));
                    if urls.len() >= MAX_REMOTE_URL_ENTRIES && !urls.contains(&entry) {
                        warnings.push(format!(
                            "Only the first {MAX_REMOTE_URL_ENTRIES} configured fetch/push URL entries were inspected"
                        ));
                        break 'remotes;
                    }
                    let _ = urls.insert(entry);
                }
            }
        }

        Ok((
            urls.into_iter()
                .map(|(remote, url)| RemoteUrl { remote, url })
                .collect(),
            warnings,
        ))
    }

    pub(super) fn resolve_github_repository(
        &self,
        url: Option<&str>,
        refresh: bool,
    ) -> Result<(GitHubRepository, CacheDisposition)> {
        let mut args = vec![OsString::from("repo"), OsString::from("view")];
        if let Some(url) = url {
            args.push(OsString::from(url));
        }
        args.extend([
            OsString::from("--json"),
            OsString::from("nameWithOwner,url"),
            OsString::from("--template"),
            OsString::from(REPOSITORY_TSV_TEMPLATE),
        ]);
        let identity = url.map_or_else(
            || {
                format!(
                    "inferred\n{}\n{}",
                    self.root.display(),
                    env::var("GH_REPO").unwrap_or_default()
                )
            },
            remote_url_for_gh,
        );
        let key = format!("repository\n{identity}");
        let response = self.checked_cached_gh(
            &key,
            CacheLife::Ttl(REPOSITORY_CACHE_TTL),
            refresh,
            args,
            "gh repo view failed",
        )?;
        let record = trim_ascii(&response.data);
        let [name_with_owner, host] =
            parse_tsv_record::<2>(record).context("invalid gh repo view output")?;
        if name_with_owner.trim().is_empty() || host.trim().is_empty() {
            bail!("gh repo view returned an incomplete repository identity");
        }
        let fields = [name_with_owner, host];
        Ok((
            GitHubRepository {
                name_with_owner: fields[0].clone(),
                url: fields[1].trim_end_matches('/').to_owned(),
                remotes: Vec::new(),
            },
            response.disposition,
        ))
    }

    pub(super) fn checked_cached_gh<I, S>(
        &self,
        cache_key: &str,
        life: CacheLife,
        refresh: bool,
        args: I,
        error_context: &str,
    ) -> Result<GhResponse>
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        self.checked_cached_gh_bounded(
            cache_key,
            life,
            refresh,
            args,
            error_context,
            MAX_GH_METADATA_BYTES,
        )
    }

    #[expect(
        clippy::too_many_arguments,
        reason = "the renderer needs the whole row context in one call"
    )]
    #[doc = " `limit` bounds both the response Quinjet will read and the entry it will"]
    #[doc = " keep, so a check log can use the cache without letting metadata grow."]
    pub(super) fn checked_cached_gh_bounded<I, S>(
        &self,
        cache_key: &str,
        life: CacheLife,
        refresh: bool,
        args: I,
        error_context: &str,
        limit: usize,
    ) -> Result<GhResponse>
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        let cache = CacheStore::discover();
        let cached = cache
            .as_ref()
            .and_then(|cache| cache.read(cache_key, limit));
        if (!refresh || life == CacheLife::Immutable)
            && let Some(entry) = cached.as_ref()
            && life.accepts(entry.age)
        {
            return Ok(GhResponse {
                data: entry.data.clone(),
                disposition: CacheDisposition::Fresh,
            });
        }

        let output = match self.run_gh_bounded(args, limit) {
            Ok(output) => output,
            Err(error) => {
                if let Some(entry) = cached.as_ref() {
                    return Ok(GhResponse {
                        data: entry.data.clone(),
                        disposition: CacheDisposition::Stale,
                    });
                }
                return Err(error);
            }
        };
        if output.status.success() && !output.stdout_truncated {
            if let Some(cache) = cache.as_ref() {
                drop(cache.write(cache_key, &output.stdout, limit));
            }
            return Ok(GhResponse {
                data: output.stdout,
                disposition: CacheDisposition::Network,
            });
        }
        if let Some(entry) = cached {
            return Ok(GhResponse {
                data: entry.data,
                disposition: CacheDisposition::Stale,
            });
        }
        if output.stdout_truncated {
            bail!("{error_context}: GitHub CLI output exceeded the metadata limit");
        }
        bail!("{}", bounded_command_error(error_context, &output));
    }
}