Editor/git/RevParse.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Sandbox.git.models;

namespace Sandbox.git;

/// <summary>
/// Rev-parse and repository-type detection. Uses a single git process where possible to avoid blocking the editor.
/// </summary>
public static class RevParse {
	const string OperationGetRepositoryType = "getRepositoryType";
	const string OperationGetUpstreamRefForRef = "getUpstreamRefForRef";

	static readonly IReadOnlySet<int> SuccessExitCodesRevParse = new HashSet<int> { 0, 128 };

	/// <summary>
	/// Attempts to fulfill the work of isGitRepository and isBareRepository while requiring only one Git process.
	/// Returns 'bare', 'regular', 'missing', or 'unsafe' (dubious ownership).
	/// </summary>
	public static async Task<RepositoryType> GetRepositoryTypeAsync(string path) {
		if ( string.IsNullOrEmpty(path) || !Directory.Exists(path) ) {
			return new MissingRepositoryType();
		}

		try {
			var result = await Core.GitAsync(
				GetRepositoryTypeArgs(),
				path,
				OperationGetRepositoryType,
				SuccessExitCodesRevParse
			).ConfigureAwait(false);

			if ( result.ExitCode == 0 ) {
				var parts = result.Stdout.Split('\n', 2);
				var isBare = parts.Length > 0 ? parts[0].Trim() : string.Empty;
				var cdup = parts.Length > 1 ? parts[1].Trim() : string.Empty;
				if ( isBare == "true" ) {
					return new BareRepositoryType();
				}

				var topLevel = Path.GetFullPath(Path.Combine(path, cdup));
				return new RegularRepositoryType(topLevel);
			}

			var unsafeMatch = Regex.Match(
				result.Stderr,
				@"fatal: detected dubious ownership in repository at '(.+)'"
			);
			if ( unsafeMatch.Success ) {
				return new UnsafeRepositoryType(unsafeMatch.Groups[1].Value);
			}

			return new MissingRepositoryType();
		} catch ( Exception ex ) {
			// ENOENT-style: path or git not found; treat as missing so we don't block the editor.
			if ( ex is FileNotFoundException or DirectoryNotFoundException ) {
				return new MissingRepositoryType();
			}

			throw;
		}
	}

	/// <summary>
	/// Resolves the upstream ref for the given ref (e.g. "refs/remotes/origin/main"). Returns null if no upstream.
	/// </summary>
	public static async Task<string> GetUpstreamRefForRefAsync(string path, string refName = null) {
		var args = GetUpstreamRefForRefArgs(refName);
		var result = await Core.GitAsync(
			args,
			path,
			OperationGetUpstreamRefForRef,
			SuccessExitCodesRevParse
		).ConfigureAwait(false);
		return result.ExitCode == 0 ? result.Stdout.Trim() : null;
	}

	/// <summary>
	/// Returns the remote name for the given ref's upstream (e.g. "origin"). Returns null if no upstream.
	/// </summary>
	public static async Task<string> GetUpstreamRemoteNameForRefAsync(string path, string refName = null) {
		var remoteRef = await GetUpstreamRefForRefAsync(path, refName).ConfigureAwait(false);
		if ( string.IsNullOrEmpty(remoteRef) ) return null;
		var match = Regex.Match(remoteRef, @"^refs/remotes/([^/]+)/");
		return match.Success ? match.Groups[1].Value : null;
	}

	/// <summary>
	/// Upstream ref for the current HEAD.
	/// </summary>
	public static Task<string> GetCurrentUpstreamRefAsync(string path) {
		return GetUpstreamRefForRefAsync(path);
	}

	/// <summary>
	/// Upstream remote name for the current HEAD.
	/// </summary>
	public static Task<string> GetCurrentUpstreamRemoteNameAsync(string path) {
		return GetUpstreamRemoteNameForRefAsync(path);
	}

	/// <summary>
	/// Current branch name (e.g. "main"), or "HEAD" if detached. Returns null if not a git repo or on failure.
	/// </summary>
	public static async Task<string> GetCurrentBranchNameAsync(string path) {
		if ( string.IsNullOrEmpty(path) )
			return null;

		var result = await Core.GitAsync(
			GetCurrentBranchNameArgs(),
			path,
			"getCurrentBranch",
			SuccessExitCodesRevParse
		).ConfigureAwait(false);

		if ( result.ExitCode != 0 )
			return null;

		var name = result.Stdout.Trim();
		return string.IsNullOrEmpty(name) ? null : name;
	}

	/// <summary>Builds git arguments for repository type detection. Exposed for testing.</summary>
	public static string[] GetRepositoryTypeArgs() {
		return new[] { "rev-parse", "--is-bare-repository", "--show-cdup" };
	}

	/// <summary>Builds git arguments for resolving upstream ref. Exposed for testing.</summary>
	public static string[] GetUpstreamRefForRefArgs(string refName = null) {
		var rev = (refName ?? string.Empty) + "@{upstream}";
		return new[] { "rev-parse", "--symbolic-full-name", rev };
	}

	/// <summary>Builds git arguments for current branch name. Exposed for testing.</summary>
	public static string[] GetCurrentBranchNameArgs() {
		return new[] { "rev-parse", "--abbrev-ref", "HEAD" };
	}
}