Skip to content

Commit

Permalink
Make showing status much better and quicker
Browse files Browse the repository at this point in the history
  • Loading branch information
geofflamrock committed Dec 20, 2024
1 parent 215b859 commit 8084392
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 149 deletions.
199 changes: 119 additions & 80 deletions src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public void Build(Repository repository, string defaultBranchName)
}
}

private static Commit CreateEmptyCommit(Repository repository, Branch branch, string message)
private static LibGit2Sharp.Commit CreateEmptyCommit(Repository repository, Branch branch, string message)
{
repository.Refs.UpdateTarget("HEAD", branch.CanonicalName);
var signature = new Signature(Some.Name(), Some.Name(), DateTimeOffset.Now);
Expand Down Expand Up @@ -238,7 +238,7 @@ public TestGitRepository Build()
return new TestGitRepository(localDirectory, remoteDirectory, localRepo);
}

private static Commit CreateInitialCommit(Repository repository)
private static LibGit2Sharp.Commit CreateInitialCommit(Repository repository)
{
var message = $"Initial commit";
var signature = new Signature(Some.Name(), Some.Name(), DateTimeOffset.Now);
Expand All @@ -252,12 +252,12 @@ public class TestGitRepository(TemporaryDirectory LocalDirectory, TemporaryDirec
public string RemoteUri => RemoteDirectory.DirectoryPath;
public GitOperationSettings GitOperationSettings => new GitOperationSettings(false, false, LocalDirectory.DirectoryPath);

public Commit GetTipOfBranch(string branchName)
public LibGit2Sharp.Commit GetTipOfBranch(string branchName)
{
return LocalRepository.Branches[branchName].Tip;
}

public List<Commit> GetCommitsReachableFromBranch(string branchName)
public List<LibGit2Sharp.Commit> GetCommitsReachableFromBranch(string branchName)
{
return [.. LocalRepository.Branches[branchName].Commits];
}
Expand Down
112 changes: 61 additions & 51 deletions src/Stack/Commands/Helpers/StackStatusHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text;
using Microsoft.VisualBasic;
using Spectre.Console;
using Stack.Config;
using Stack.Git;
Expand All @@ -8,14 +9,14 @@ namespace Stack.Commands.Helpers;

public class BranchDetail
{
public BranchStatus Status { get; set; } = new(false, false, 0, 0, 0, 0);
public BranchStatus Status { get; set; } = new(false, false, false, 0, 0, 0, 0, null);
public GitHubPullRequest? PullRequest { get; set; }

public bool IsActive => Status.ExistsLocally && Status.ExistsInRemote && (PullRequest is null || PullRequest.State != GitHubPullRequestStates.Merged);
public bool CouldBeCleanedUp => Status.ExistsLocally && (!Status.ExistsInRemote || PullRequest is not null && PullRequest.State == GitHubPullRequestStates.Merged);
public bool HasPullRequest => PullRequest is not null && PullRequest.State != GitHubPullRequestStates.Closed;
}
public record BranchStatus(bool ExistsLocally, bool ExistsInRemote, int AheadOfParent, int BehindParent, int AheadOfRemote, int BehindRemote);
public record BranchStatus(bool ExistsLocally, bool ExistsInRemote, bool IsCurrentBranch, int AheadOfParent, int BehindParent, int AheadOfRemote, int BehindRemote, Commit? Tip);
public record StackStatus(Dictionary<string, BranchDetail> Branches)
{
public string[] GetActiveBranches() => Branches.Where(b => b.Value.IsActive).Select(b => b.Key).ToArray();
Expand All @@ -29,6 +30,7 @@ public static class StackStatusHelpers
IOutputProvider outputProvider,
IGitOperations gitOperations,
IGitHubOperations gitHubOperations,
bool includeParentBranchStatus = true,
bool includePullRequestStatus = true)
{
var stacksToCheckStatusFor = new Dictionary<Config.Stack, StackStatus>();
Expand All @@ -40,44 +42,37 @@ public static class StackStatusHelpers

var allBranchesInStacks = stacks.SelectMany(s => new List<string>([s.SourceBranch]).Concat(s.Branches)).Distinct().ToArray();

outputProvider.Status("Checking status of branches...", () =>
var branchStatuses = gitOperations.GetBranchStatuses(allBranchesInStacks);

foreach (var (stack, status) in stacksToCheckStatusFor)
{
var branchesThatExistInRemote = gitOperations.GetBranchesThatExistInRemote(allBranchesInStacks);
var branchesThatExistLocally = gitOperations.GetBranchesThatExistLocally(allBranchesInStacks);
var parentBranch = stack.SourceBranch;

foreach (var (stack, status) in stacksToCheckStatusFor)
status.Branches.Add(stack.SourceBranch, new BranchDetail());
branchStatuses.TryGetValue(stack.SourceBranch, out var sourceBranchStatus);
if (sourceBranchStatus is not null)
{
void CheckBranchStatus(string branch, string sourceBranch)
{
var branchExistsLocally = branchesThatExistLocally.Contains(branch);
var (ahead, behind) = gitOperations.CompareBranches(branch, sourceBranch);
var (aheadRemote, behindRemote) = gitOperations.GetComparisonToRemoteTrackingBranch(branch);
var branchStatus = new BranchStatus(branchExistsLocally, true, ahead, behind, aheadRemote, behindRemote);
status.Branches[branch].Status = branchStatus;
}

var parentBranch = stack.SourceBranch;
status.Branches[stack.SourceBranch].Status = new BranchStatus(true, sourceBranchStatus.RemoteBranchExists, sourceBranchStatus.IsCurrentBranch, 0, 0, sourceBranchStatus.Ahead, sourceBranchStatus.Behind, sourceBranchStatus.Tip);
}

status.Branches.Add(stack.SourceBranch, new BranchDetail());
var sourceBranchRemoteStatus = gitOperations.GetComparisonToRemoteTrackingBranch(stack.SourceBranch);
status.Branches[stack.SourceBranch].Status = new BranchStatus(branchesThatExistLocally.Contains(stack.SourceBranch), true, 0, 0, sourceBranchRemoteStatus.Ahead, sourceBranchRemoteStatus.Behind);
foreach (var branch in stack.Branches)
{
status.Branches.Add(branch, new BranchDetail());
branchStatuses.TryGetValue(branch, out var branchStatus);

foreach (var branch in stack.Branches)
if (branchStatus is not null)
{
status.Branches.Add(branch, new BranchDetail());
var (aheadOfParent, behindParent) = includeParentBranchStatus && branchStatus.RemoteBranchExists ? gitOperations.CompareBranches(branch, parentBranch) : (0, 0);

if (branchesThatExistInRemote.Contains(branch))
status.Branches[branch].Status = new BranchStatus(true, branchStatus.RemoteBranchExists, branchStatus.IsCurrentBranch, aheadOfParent, behindParent, branchStatus.Ahead, branchStatus.Behind, branchStatus.Tip);

if (branchStatus.RemoteBranchExists)
{
CheckBranchStatus(branch, parentBranch);
parentBranch = branch;
}
else
{
status.Branches[branch].Status = new BranchStatus(branchesThatExistLocally.Contains(branch), false, 0, 0, 0, 0);
}
}
}
});
}

if (includePullRequestStatus)
{
Expand Down Expand Up @@ -122,22 +117,25 @@ public static StackStatus GetStackStatus(

public static void OutputStackStatus(
Dictionary<Config.Stack, StackStatus> stackStatuses,
IGitOperations gitOperations,
IOutputProvider outputProvider)
{
foreach (var (stack, status) in stackStatuses)
{
OutputStackStatus(stack, status, gitOperations, outputProvider);
OutputStackStatus(stack, status, outputProvider);
outputProvider.NewLine();
}
}

public static void OutputStackStatus(
Config.Stack stack,
StackStatus status,
IGitOperations gitOperations,
IOutputProvider outputProvider)
{
var header = $"{stack.Name.Stack()}: {stack.SourceBranch.Muted()}";
var header = stack.SourceBranch.Branch();
if (status.Branches.TryGetValue(stack.SourceBranch, out var sourceBranchStatus))
{
header = GetBranchStatusOutput(stack.SourceBranch, null, sourceBranchStatus);
}
var items = new List<string>();

string parentBranch = stack.SourceBranch;
Expand All @@ -146,25 +144,25 @@ public static void OutputStackStatus(
{
if (status.Branches.TryGetValue(branch, out var branchDetail))
{
items.Add(GetBranchAndPullRequestStatusOutput(branch, parentBranch, branchDetail, gitOperations));
items.Add(GetBranchAndPullRequestStatusOutput(branch, parentBranch, branchDetail));

if (branchDetail.IsActive)
{
parentBranch = branch;
}
}
}
outputProvider.Information(stack.Name.Stack());
outputProvider.Tree(header, [.. items]);
}

public static string GetBranchAndPullRequestStatusOutput(
string branch,
string parentBranch,
BranchDetail branchDetail,
IGitOperations gitOperations)
string? parentBranch,
BranchDetail branchDetail)
{
var branchNameBuilder = new StringBuilder();
branchNameBuilder.Append(GetBranchStatusOutput(branch, parentBranch, branchDetail, gitOperations));
branchNameBuilder.Append(GetBranchStatusOutput(branch, parentBranch, branchDetail));

if (branchDetail.PullRequest is not null)
{
Expand All @@ -176,53 +174,65 @@ public static string GetBranchAndPullRequestStatusOutput(

public static string GetBranchStatusOutput(
string branch,
string parentBranch,
BranchDetail branchDetail,
IGitOperations gitOperations)
string? parentBranch,
BranchDetail branchDetail)
{
var branchNameBuilder = new StringBuilder();
var currentBranch = gitOperations.GetCurrentBranch();

var color = !branchDetail.IsActive ? "grey" : branch.Equals(currentBranch, StringComparison.OrdinalIgnoreCase) ? "blue" : null;
Decoration? decoration = !branchDetail.IsActive ? Decoration.Strikethrough : null;
var branchName = branchDetail.Status.IsCurrentBranch ? $"* [{Color.Green}]{branch}[/]" : branch;
Color? color = branchDetail.Status.ExistsLocally ? null : Color.Grey;
Decoration? decoration = branchDetail.Status.ExistsLocally ? null : Decoration.Strikethrough;

if (color is not null && decoration is not null)
{
branchNameBuilder.Append($"[{decoration} {color}]{branch}[/]");
branchNameBuilder.Append($"[{decoration} {color}]{branchName}[/]");
}
else if (color is not null)
{
branchNameBuilder.Append($"[{color}]{branch}[/]");
branchNameBuilder.Append($"[{color}]{branchName}[/]");
}
else if (decoration is not null)
{
branchNameBuilder.Append($"[{decoration}]{branch}[/]");
branchNameBuilder.Append($"[{decoration}]{branchName}[/]");
}
else
{
branchNameBuilder.Append(branch);
branchNameBuilder.Append(branchName);
}

if (branchDetail.IsActive)
{
if (branchDetail.Status.AheadOfRemote > 0 || branchDetail.Status.BehindRemote > 0)
{
branchNameBuilder.Append($" {branchDetail.Status.BehindRemote}{Emoji.Known.DownArrow}{branchDetail.Status.AheadOfRemote}{Emoji.Known.UpArrow}".Muted());
branchNameBuilder.Append($" {branchDetail.Status.BehindRemote}{Emoji.Known.DownArrow}{branchDetail.Status.AheadOfRemote}{Emoji.Known.UpArrow}");
}

if (branchDetail.Status.AheadOfParent > 0 && branchDetail.Status.BehindParent > 0)
{
branchNameBuilder.Append($" [grey]({branchDetail.Status.AheadOfParent} ahead, {branchDetail.Status.BehindParent} behind {parentBranch})[/]");
branchNameBuilder.Append($" ({branchDetail.Status.AheadOfParent} ahead, {branchDetail.Status.BehindParent} behind {parentBranch})".Muted());
}
else if (branchDetail.Status.AheadOfParent > 0)
{
branchNameBuilder.Append($" [grey]({branchDetail.Status.AheadOfParent} ahead of {parentBranch})[/]");
branchNameBuilder.Append($" ({branchDetail.Status.AheadOfParent} ahead of {parentBranch})".Muted());
}
else if (branchDetail.Status.BehindParent > 0)
{
branchNameBuilder.Append($" [grey]({branchDetail.Status.BehindParent} behind {parentBranch})[/]");
branchNameBuilder.Append($" ({branchDetail.Status.BehindParent} behind {parentBranch})".Muted());
}
}
else if (branchDetail.Status.ExistsLocally && !branchDetail.Status.ExistsInRemote)
{
branchNameBuilder.Append(" (remote branch deleted)".Muted());
}
else if (branchDetail.PullRequest is not null && branchDetail.PullRequest.State == GitHubPullRequestStates.Merged)
{
branchNameBuilder.Append(" (pull request merged)".Muted());
}

if (branchDetail.Status.Tip is not null)
{
branchNameBuilder.Append($" {branchDetail.Status.Tip.Sha[..7].Commit()} {branchDetail.Status.Tip.Message}");
}

return branchNameBuilder.ToString();
}
Expand Down
10 changes: 5 additions & 5 deletions src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public async Task<CreatePullRequestsCommandResponse> Handle(CreatePullRequestsCo
}
}

StackStatusHelpers.OutputStackStatus(stack, status, gitOperations, outputProvider);
StackStatusHelpers.OutputStackStatus(stack, status, outputProvider);

outputProvider.NewLine();

Expand All @@ -112,7 +112,7 @@ public async Task<CreatePullRequestsCommandResponse> Handle(CreatePullRequestsCo

outputProvider.NewLine();

OutputUpdatedStackStatus(outputProvider, gitOperations, stack, status, pullRequestCreateActions);
OutputUpdatedStackStatus(outputProvider, stack, status, pullRequestCreateActions);

outputProvider.NewLine();

Expand Down Expand Up @@ -229,7 +229,7 @@ private static List<GitHubPullRequest> CreatePullRequests(
return pullRequests;
}

private static void OutputUpdatedStackStatus(IOutputProvider outputProvider, IGitOperations gitOperations, Config.Stack stack, StackStatus status, List<GitHubPullRequestCreateAction> pullRequestCreateActions)
private static void OutputUpdatedStackStatus(IOutputProvider outputProvider, Config.Stack stack, StackStatus status, List<GitHubPullRequestCreateAction> pullRequestCreateActions)
{
var branchDisplayItems = new List<string>();
var parentBranch = stack.SourceBranch;
Expand All @@ -239,12 +239,12 @@ private static void OutputUpdatedStackStatus(IOutputProvider outputProvider, IGi
var branchDetail = status.Branches[branch];
if (branchDetail.PullRequest is not null && branchDetail.PullRequest.State != GitHubPullRequestStates.Closed)
{
branchDisplayItems.Add(StackStatusHelpers.GetBranchAndPullRequestStatusOutput(branch, parentBranch, branchDetail, gitOperations));
branchDisplayItems.Add(StackStatusHelpers.GetBranchAndPullRequestStatusOutput(branch, parentBranch, branchDetail));
}
else
{
var action = pullRequestCreateActions.FirstOrDefault(a => a.HeadBranch == branch);
branchDisplayItems.Add($"{StackStatusHelpers.GetBranchStatusOutput(branch, parentBranch, branchDetail, gitOperations)} *NEW* {action?.Title}{(action?.Draft == true ? " (draft)".Muted() : string.Empty)}");
branchDisplayItems.Add($"{StackStatusHelpers.GetBranchStatusOutput(branch, parentBranch, branchDetail)} *NEW* {action?.Title}{(action?.Draft == true ? " (draft)".Muted() : string.Empty)}");
}
parentBranch = branch;
}
Expand Down
11 changes: 8 additions & 3 deletions src/Stack/Commands/Stack/StackStatusCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public class StackStatusCommandSettings : CommandSettingsBase
[CommandOption("--all")]
public bool All { get; init; }

[Description("Show minimal status.")]
[CommandOption("--minimal")]
public bool Minimal { get; init; }

[Description("Show full status including pull requests.")]
[CommandOption("--full")]
public bool Full { get; init; }
Expand All @@ -37,13 +41,13 @@ public override async Task<int> ExecuteAsync(CommandContext context, StackStatus
new GitHubOperations(outputProvider, settings.GetGitHubOperationSettings()),
new StackConfig());

await handler.Handle(new StackStatusCommandInputs(settings.Name, settings.All, settings.Full));
await handler.Handle(new StackStatusCommandInputs(settings.Name, settings.All, settings.Minimal, settings.Full));

return 0;
}
}

public record StackStatusCommandInputs(string? Name, bool All, bool Full);
public record StackStatusCommandInputs(string? Name, bool All, bool Minimal, bool Full);
public record StackStatusCommandResponse(Dictionary<Config.Stack, StackStatus> Statuses);

public class StackStatusCommandHandler(
Expand Down Expand Up @@ -86,9 +90,10 @@ public async Task<StackStatusCommandResponse> Handle(StackStatusCommandInputs in
outputProvider,
gitOperations,
gitHubOperations,
!inputs.Minimal,
inputs.Full);

StackStatusHelpers.OutputStackStatus(stackStatusResults, gitOperations, outputProvider);
StackStatusHelpers.OutputStackStatus(stackStatusResults, outputProvider);

if (stacksToCheckStatusFor.Count == 1)
{
Expand Down
Loading

0 comments on commit 8084392

Please sign in to comment.