diff --git a/src/Stack.Tests/Commands/Helpers/StackHelpersTests.cs b/src/Stack.Tests/Commands/Helpers/StackHelpersTests.cs index 1298711b..ad62678e 100644 --- a/src/Stack.Tests/Commands/Helpers/StackHelpersTests.cs +++ b/src/Stack.Tests/Commands/Helpers/StackHelpersTests.cs @@ -162,7 +162,7 @@ public void UpdateStackUsingRebase_WhenARemoteBranchIsDeleted_RebasesOntoThePare "Stack1", Some.HttpsUri().ToString(), sourceBranch, - [new Config.Branch(branch1, []), new Config.Branch(branch2, [])] + [new Config.Branch(branch1, [new Config.Branch(branch2, [])])] ); var stackStatus = StackHelpers.GetStackStatus(stack, branch1, logger, gitClient, gitHubClient, false); @@ -238,7 +238,7 @@ public void UpdateStackUsingRebase_WhenARemoteBranchIsDeleted_ButTheTargetBranch "Stack1", Some.HttpsUri().ToString(), sourceBranch, - [new Config.Branch(branch1, []), new Config.Branch(branch2, [])] + [new Config.Branch(branch1, [new Config.Branch(branch2, [])])] ); var stackStatus = StackHelpers.GetStackStatus(stack, branch1, logger, gitClient, gitHubClient, false); @@ -292,7 +292,7 @@ public void UpdateStackUsingRebase_WhenARemoteBranchIsDeleted_AndLocalBranchIsDe "Stack1", Some.HttpsUri().ToString(), sourceBranch, - [new Config.Branch(branch1, []), new Config.Branch(branch2, [])] + [new Config.Branch(branch1, [new Config.Branch(branch2, [])])] ); var stackStatus = StackHelpers.GetStackStatus(stack, branch1, logger, gitClient, gitHubClient, false); @@ -306,4 +306,132 @@ public void UpdateStackUsingRebase_WhenARemoteBranchIsDeleted_AndLocalBranchIsDe var fileContents = File.ReadAllText(Path.Join(repo.LocalDirectoryPath, changedFile2Path)); fileContents.Should().Be(changedFile2Contents); } + + [Fact] + public void UpdateStackUsingRebase_WhenStackHasATreeStructure_RebasesAllBranchesCorrectly() + { + // Arrange + var sourceBranch = "source-branch"; + var branch1 = "branch-1"; + var branch2 = "branch-2"; + var branch3 = "branch-3"; + var changedFilePath = "change-file-1"; + var commit1ChangedFileContents = "These are the changes in the first commit"; + + using var repo = new TestGitRepositoryBuilder() + .WithBranch(b => b + .WithName(sourceBranch) + .PushToRemote()) + .WithBranch(b => b + .WithName(branch1) + .FromSourceBranch(sourceBranch) + .WithCommit(c => c.WithChanges("file-1", "file-1-changes")) + .PushToRemote()) + .WithBranch(b => b + .WithName(branch2) + .FromSourceBranch(branch1) + .WithCommit(c => c.WithChanges("file-2", "file-2-changes")) + .PushToRemote()) + .WithBranch(b => b + .WithName(branch3) + .FromSourceBranch(branch1) + .WithCommit(c => c.WithChanges("file-3", "file-3-changes")) + .PushToRemote()) + .Build(); + + var inputProvider = Substitute.For(); + var gitClient = new GitClient(new TestLogger(testOutputHelper), repo.GitClientSettings); + var logger = new TestLogger(testOutputHelper); + var gitHubClient = Substitute.For(); + + gitClient.ChangeBranch(sourceBranch); + File.WriteAllText(Path.Join(repo.LocalDirectoryPath, changedFilePath), commit1ChangedFileContents); + repo.Stage(changedFilePath); + repo.Commit(); + repo.Push(sourceBranch); + + var stack = new Config.Stack( + "Stack1", + Some.HttpsUri().ToString(), + sourceBranch, + [new Config.Branch(branch1, [new Config.Branch(branch2, []), new Config.Branch(branch3, [])])] + ); + var stackStatus = StackHelpers.GetStackStatus(stack, sourceBranch, logger, gitClient, gitHubClient, false); + + // Act + StackHelpers.UpdateStackUsingRebase(stack, stackStatus, gitClient, inputProvider, logger); + + // Assert + gitClient.ChangeBranch(branch2); + var branch2FileContents = File.ReadAllText(Path.Join(repo.LocalDirectoryPath, changedFilePath)); + branch2FileContents.Should().Be(commit1ChangedFileContents); + + gitClient.ChangeBranch(branch3); + var branch3FileContents = File.ReadAllText(Path.Join(repo.LocalDirectoryPath, changedFilePath)); + branch3FileContents.Should().Be(commit1ChangedFileContents); + } + + [Fact] + public void UpdateStackUsingMerge_WhenStackHasATreeStructure_MergesAllBranchesCorrectly() + { + // Arrange + var sourceBranch = "source-branch"; + var branch1 = "branch-1"; + var branch2 = "branch-2"; + var branch3 = "branch-3"; + var changedFilePath = "change-file-1"; + var commit1ChangedFileContents = "These are the changes in the first commit"; + + using var repo = new TestGitRepositoryBuilder() + .WithBranch(b => b + .WithName(sourceBranch) + .PushToRemote()) + .WithBranch(b => b + .WithName(branch1) + .FromSourceBranch(sourceBranch) + .WithCommit(c => c.WithChanges("file-1", "file-1-changes")) + .PushToRemote()) + .WithBranch(b => b + .WithName(branch2) + .FromSourceBranch(branch1) + .WithCommit(c => c.WithChanges("file-2", "file-2-changes")) + .PushToRemote()) + .WithBranch(b => b + .WithName(branch3) + .FromSourceBranch(branch1) + .WithCommit(c => c.WithChanges("file-3", "file-3-changes")) + .PushToRemote()) + .Build(); + + var inputProvider = Substitute.For(); + var gitClient = new GitClient(new TestLogger(testOutputHelper), repo.GitClientSettings); + var logger = new TestLogger(testOutputHelper); + var gitHubClient = Substitute.For(); + + gitClient.ChangeBranch(sourceBranch); + File.WriteAllText(Path.Join(repo.LocalDirectoryPath, changedFilePath), commit1ChangedFileContents); + repo.Stage(changedFilePath); + repo.Commit(); + repo.Push(sourceBranch); + + var stack = new Config.Stack( + "Stack1", + Some.HttpsUri().ToString(), + sourceBranch, + [new Config.Branch(branch1, [new Config.Branch(branch2, []), new Config.Branch(branch3, [])])] + ); + var stackStatus = StackHelpers.GetStackStatus(stack, sourceBranch, logger, gitClient, gitHubClient, false); + + // Act + StackHelpers.UpdateStackUsingMerge(stack, stackStatus, gitClient, inputProvider, logger); + + // Assert + gitClient.ChangeBranch(branch2); + var branch2FileContents = File.ReadAllText(Path.Join(repo.LocalDirectoryPath, changedFilePath)); + branch2FileContents.Should().Be(commit1ChangedFileContents); + + gitClient.ChangeBranch(branch3); + var branch3FileContents = File.ReadAllText(Path.Join(repo.LocalDirectoryPath, changedFilePath)); + branch3FileContents.Should().Be(commit1ChangedFileContents); + } } \ No newline at end of file diff --git a/src/Stack.Tests/Config/StackTests.cs b/src/Stack.Tests/Config/StackTests.cs index de3f970c..3a0869b1 100644 --- a/src/Stack.Tests/Config/StackTests.cs +++ b/src/Stack.Tests/Config/StackTests.cs @@ -7,6 +7,49 @@ namespace Stack.Tests; public class StackTests { + [Fact] + public void GetAllBranchLines_ReturnsAllRootToLeafPaths() + { + // Arrange: Build a stack with the following structure: + // - A + // - B + // - C + // - D + // - E + // - F + // - G + var stack = new Config.Stack( + "TestStack", + Some.HttpsUri().ToString(), + "main", + [ + new Config.Branch("A", [ + new Config.Branch("B", [ + new Config.Branch("C", []), + new Config.Branch("D", []) + ]), + new Config.Branch("E", []), + new Config.Branch("F", [ + new Config.Branch("G", []) + ]) + ]) + ] + ); + + // Act + var lines = stack.GetAllBranchLines(); + + // Assert: Should match the expected root-to-leaf paths (by branch name) + var branchNameLines = lines.Select(line => line.Select(b => b.Name).ToArray()).ToList(); + branchNameLines.Should().BeEquivalentTo( + [ + ["A", "B", "C"], + ["A", "B", "D"], + ["A", "E"], + ["A", "F", "G"] + ], options => options.WithStrictOrdering()); + } + [Fact] public void GetDefaultBranchName_WhenNoBranchesInStack_ShouldReturnBranchNameWithTheNumber1AtTheEnd_BecauseItIsTheFirstBranch() { diff --git a/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs b/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs index aaf36318..fe4c95c6 100644 --- a/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs +++ b/src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs @@ -1,3 +1,4 @@ +using System.Text; using LibGit2Sharp; using Stack.Git; @@ -84,13 +85,8 @@ public class CommitBuilder { Func? getBranchName; string? message; - string? authorName; - string? authorEmail; - string? committerName; - string? committerEmail; - bool allowEmptyCommit; bool pushToRemote; - List<(string Path, string Contents)> changes = []; + List<(string Path, string? Contents)> changes = []; public CommitBuilder OnBranch(string branch) { @@ -116,26 +112,6 @@ public CommitBuilder WithMessage(string message) return this; } - public CommitBuilder WithAuthor(string name, string email) - { - authorName = name; - authorEmail = email; - return this; - } - - public CommitBuilder WithCommitter(string name, string email) - { - committerName = name; - committerEmail = email; - return this; - } - - public CommitBuilder AllowEmptyCommit() - { - allowEmptyCommit = true; - return this; - } - public CommitBuilder PushToRemote() { pushToRemote = true; @@ -152,29 +128,80 @@ public void Build(Repository repository) branch = repository.Branches[branchName]; } - if (branch is not null) + var signature = new Signature(Some.Name(), Some.Name(), DateTimeOffset.Now); + + Commit(repository, branch?.Tip, branch?.CanonicalName, message ?? Some.Name(), signature, changes.ToArray()); + + if (branch is not null && pushToRemote) { - repository.Refs.UpdateTarget("HEAD", branch.CanonicalName); + repository.Network.Push(branch); } + } - foreach (var (path, contents) in changes) + public static LibGit2Sharp.Commit Commit(Repository repository, + LibGit2Sharp.Commit? parent, + string? branchName, + string message, + Signature? signature, + params (string Name, string? Content)[] files) + { + // Commits for uninitialised repositories will have no parent, and will need to start with an empty tree. + var treeDefinition = parent is null ? new TreeDefinition() : TreeDefinition.From(parent.Tree); + + foreach (var file in files) { - var fullPath = Path.Combine(repository.Info.WorkingDirectory, path); - var directory = Path.GetDirectoryName(fullPath); - Directory.CreateDirectory(directory!); - File.WriteAllText(fullPath, contents); - LibGit2Sharp.Commands.Stage(repository, path); + if (file.Content is null) + { + treeDefinition.Remove(file.Name); + } + else + { + var bytes = Encoding.UTF8.GetBytes(file.Content); + var blobId = repository.ObjectDatabase.Write(bytes); + treeDefinition.Add(file.Name, blobId, Mode.NonExecutableFile); + } } - var signature = new Signature(authorName ?? Some.Name(), authorEmail ?? Some.Name(), DateTimeOffset.Now); - var committer = new Signature(committerName ?? Some.Name(), committerEmail ?? Some.Name(), DateTimeOffset.Now); + return CommitTreeDefinition(repository, parent, branchName, message, signature, treeDefinition); + } - repository.Commit(message ?? Some.Name(), signature, committer, new CommitOptions() { AllowEmptyCommit = allowEmptyCommit }); + static LibGit2Sharp.Commit CommitTreeDefinition(Repository repository, + LibGit2Sharp.Commit? parent, + string? branchName, + string message, + Signature? signature, + TreeDefinition treeDefinition) + { + // Write the tree to the object database + var tree = repository.ObjectDatabase.CreateTree(treeDefinition); - if (branch is not null && pushToRemote) + // Create the commit + var parents = parent is null ? Array.Empty() : new[] { parent }; + var commit = repository.ObjectDatabase.CreateCommit( + signature, + signature, + message, + tree, + parents, + false); + + if (branchName is not null) { - repository.Network.Push(branch); + // Point the branch at the new commit if a branch name + // has been provided + var branch = repository.Branches[branchName]; + + if (branch is null) + { + repository.Branches.Add(branchName, commit); + } + else + { + repository.Refs.UpdateTarget(branch.Reference, commit.Id); + } } + + return commit; } } @@ -216,7 +243,7 @@ public TestGitRepositoryBuilder WithNumberOfEmptyCommits(string branchName, int { commitBuilders.Add(b => { - b.OnBranch(branchName).WithMessage($"Empty commit {i + 1}").AllowEmptyCommit(); + b.OnBranch(branchName).WithMessage($"Empty commit {i + 1}"); if (pushToRemote) { @@ -234,7 +261,6 @@ public TestGitRepositoryBuilder WithNumberOfEmptyCommits(Action c commitBuilders.Add(b => { commitBuilder(b); - b.AllowEmptyCommit(); }); } return this; @@ -248,7 +274,6 @@ public TestGitRepositoryBuilder WithNumberOfEmptyCommitsOnRemoteTrackingBranchOf { commitBuilder(b); b.OnBranch(r => r.Branches[branch].TrackedBranch.CanonicalName); - b.AllowEmptyCommit(); }); } return this; diff --git a/src/Stack/Commands/Helpers/StackHelpers.cs b/src/Stack/Commands/Helpers/StackHelpers.cs index a5b39767..79323c51 100644 --- a/src/Stack/Commands/Helpers/StackHelpers.cs +++ b/src/Stack/Commands/Helpers/StackHelpers.cs @@ -43,87 +43,6 @@ static List GetAllBranches(BranchDetail branch) return branchesToReturn; } - - public BranchDetail? GetDeepestActiveBranchInSingleTree() - { - foreach (var branch in Branches) - { - var deepestBranch = GetDeepestActiveBranch(branch); - if (deepestBranch is not null) - { - return deepestBranch; - } - } - - return null; - } - - public BranchDetail? GetDeepestActiveBranch(BranchDetail branch) - { - if (branch.IsActive && branch.Children.Count == 0) - { - return branch; - } - - foreach (var child in branch.Children) - { - if (child.IsActive) - { - var deepestChild = GetDeepestActiveBranch(child); - if (deepestChild is not null) - { - return deepestChild; - } - } - } - - return null; - } - - public BranchDetail? WalkBranches(Func predicate) - { - static BranchDetail? WalkBranch(Func predicate, BranchDetail branch) - { - var action = predicate(branch); - if (action == WalkAction.Return) - { - return branch; - } - else if (action == WalkAction.Skip) - { - return null; - } - - foreach (var child in branch.Children) - { - var result = WalkBranch(predicate, child); - if (result is not null) - { - return result; - } - } - - return null; - } - - foreach (var branch in Branches) - { - var result = WalkBranch(predicate, branch); - if (result is not null) - { - return result; - } - } - - return null; - } -} - -public enum WalkAction -{ - Continue, - Return, - Skip } public record RemoteTrackingBranchStatus(string Name, bool Exists, int Ahead, int Behind); @@ -515,29 +434,29 @@ public static string GetBranchStatusOutput(BranchDetail branch) } public static void OutputBranchAndStackActions( - StackStatus stack, + StackStatus status, ILogger logger) { - var allBranches = stack.GetAllBranches(); + var allBranches = status.GetAllBranches(); if (allBranches.All(branch => branch.CouldBeCleanedUp)) { logger.Information("All branches exist locally but are either not in the remote repository or the pull request associated with the branch is no longer open. This stack might be able to be deleted."); logger.NewLine(); - logger.Information($"Run {$"stack delete --stack \"{stack.Name}\"".Example()} to delete the stack if it's no longer needed."); + logger.Information($"Run {$"stack delete --stack \"{status.Name}\"".Example()} to delete the stack if it's no longer needed."); logger.NewLine(); } else if (allBranches.Any(branch => branch.CouldBeCleanedUp)) { logger.Information("Some branches exist locally but are either not in the remote repository or the pull request associated with the branch is no longer open."); logger.NewLine(); - logger.Information($"Run {$"stack cleanup --stack \"{stack.Name}\"".Example()} to clean up local branches."); + logger.Information($"Run {$"stack cleanup --stack \"{status.Name}\"".Example()} to clean up local branches."); logger.NewLine(); } else if (allBranches.All(branch => !branch.Exists)) { logger.Information("No branches exist locally. This stack might be able to be deleted."); logger.NewLine(); - logger.Information($"Run {$"stack delete --stack \"{stack.Name}\"".Example()} to delete the stack."); + logger.Information($"Run {$"stack delete --stack \"{status.Name}\"".Example()} to delete the stack."); logger.NewLine(); } @@ -545,7 +464,7 @@ public static void OutputBranchAndStackActions( { logger.Information("There are changes in local branches that have not been pushed to the remote repository."); logger.NewLine(); - logger.Information($"Run {$"stack push --stack \"{stack.Name}\"".Example()} to push the changes to the remote repository."); + logger.Information($"Run {$"stack push --stack \"{status.Name}\"".Example()} to push the changes to the remote repository."); logger.NewLine(); } @@ -553,9 +472,9 @@ public static void OutputBranchAndStackActions( { logger.Information("There are changes in source branches that have not been applied to the stack."); logger.NewLine(); - logger.Information($"Run {$"stack update --stack \"{stack.Name}\"".Example()} to update the stack locally."); + logger.Information($"Run {$"stack update --stack \"{status.Name}\"".Example()} to update the stack locally."); logger.NewLine(); - logger.Information($"Run {$"stack sync --stack \"{stack.Name}\"".Example()} to sync the stack with the remote repository."); + logger.Information($"Run {$"stack sync --stack \"{status.Name}\"".Example()} to sync the stack with the remote repository."); logger.NewLine(); } } @@ -620,31 +539,33 @@ public static void UpdateStackUsingMerge( IInputProvider inputProvider, ILogger logger) { - foreach (var branch in status.Branches) + var allBranchLines = status.GetAllBranchLines(); + + foreach (var branchLine in allBranchLines) { - UpdateBranchUsingMerge(branch, status.SourceBranch, gitClient, inputProvider, logger); + UpdateBranchLineUsingMerge(branchLine, status.SourceBranch, gitClient, inputProvider, logger); } } - public static void UpdateBranchUsingMerge( - BranchDetail branch, + public static void UpdateBranchLineUsingMerge( + List branchLine, BranchDetailBase parentBranch, IGitClient gitClient, IInputProvider inputProvider, ILogger logger) { - if (branch.IsActive) - { - MergeFromSourceBranch(branch.Name, parentBranch.Name, gitClient, inputProvider, logger); - } - else + var currentParentBranch = parentBranch; + foreach (var branch in branchLine) { - logger.Debug($"Branch '{branch}' no longer exists on the remote repository or the associated pull request is no longer open. Skipping..."); - } - - foreach (var child in branch.Children) - { - UpdateBranchUsingMerge(child, branch.IsActive ? branch : parentBranch, gitClient, inputProvider, logger); + if (branch.IsActive) + { + MergeFromSourceBranch(branch.Name, currentParentBranch.Name, gitClient, inputProvider, logger); + currentParentBranch = branch; + } + else + { + logger.Debug($"Branch '{branch}' no longer exists on the remote repository or the associated pull request is no longer open. Skipping..."); + } } } @@ -825,6 +746,16 @@ public static void UpdateStackUsingRebase( IGitClient gitClient, IInputProvider inputProvider, ILogger logger) + { + var allBranchLines = status.GetAllBranchLines(); + + foreach (var branchLine in allBranchLines) + { + UpdateBranchLineUsingRebase(status, gitClient, inputProvider, logger, branchLine); + } + } + + private static void UpdateBranchLineUsingRebase(StackStatus status, IGitClient gitClient, IInputProvider inputProvider, ILogger logger, List branchLine) { // // When rebasing the stack, we'll use `git rebase --update-refs` from the @@ -852,39 +783,38 @@ public static void UpdateStackUsingRebase( // We'll rebase feature3 onto feature2 using a normal `git rebase feature2 --update-refs`, // then feature3 onto main using `git rebase --onto main feature1 --update-refs` to replay // all commits from feature3 (and therefore from feature2) on top of the latest commits of main - // which will include the squashed commit. + // which will include the squashed commit. // - var lowestActionBranch = status.WalkBranches(b => + logger.Information($"Rebasing stack {status.Name.Stack()} for branch line: {status.SourceBranch.Name.Branch()} --> {string.Join(" -> ", branchLine.Select(b => b.Name.Branch()))}"); + + BranchDetail? lowestActionBranch = null; + foreach (var branch in branchLine) { - if (b.IsActive && b.Children.Count == 0) + if (branch.IsActive) { - return WalkAction.Return; + lowestActionBranch = branch; } + } - return WalkAction.Continue; - }); - - string? branchToRebaseFrom = lowestActionBranch?.Name; - string? lowestInactiveBranchToReParentFrom = null; - - if (branchToRebaseFrom is null) + if (lowestActionBranch is null) { logger.Warning("No active branches found in the stack."); return; } - var branchesToRebaseOnto = new List(stack.AllBranchNames); + string? branchToRebaseFrom = lowestActionBranch.Name; + string? lowestInactiveBranchToReParentFrom = null; + + List branchesToRebaseOnto = [.. branchLine]; branchesToRebaseOnto.Reverse(); - branchesToRebaseOnto.Remove(branchToRebaseFrom); - branchesToRebaseOnto.Add(stack.SourceBranch); + branchesToRebaseOnto.Remove(lowestActionBranch); + branchesToRebaseOnto.Add(status.SourceBranch); - List allBranchesInStack = [status.SourceBranch, .. status.GetAllBranches()]; + List allBranchesInStack = [status.SourceBranch, .. branchLine]; foreach (var branchToRebaseOnto in branchesToRebaseOnto) { - var branchDetail = allBranchesInStack.First(b => b.Name == branchToRebaseOnto); - - if (branchDetail.IsActive) + if (branchToRebaseOnto.IsActive) { var lowestInactiveBranchToReParentFromDetail = lowestInactiveBranchToReParentFrom is not null ? allBranchesInStack.First(b => b.Name == lowestInactiveBranchToReParentFrom) : null; var shouldRebaseOntoParent = lowestInactiveBranchToReParentFromDetail is not null && lowestInactiveBranchToReParentFromDetail.Exists; @@ -896,16 +826,16 @@ public static void UpdateStackUsingRebase( if (shouldRebaseOntoParent) { - RebaseOntoNewParent(branchToRebaseFrom, branchToRebaseOnto, lowestInactiveBranchToReParentFrom!, gitClient, inputProvider, logger); + RebaseOntoNewParent(branchToRebaseFrom, branchToRebaseOnto.Name, lowestInactiveBranchToReParentFrom!, gitClient, inputProvider, logger); } else { - RebaseFromSourceBranch(branchToRebaseFrom, branchToRebaseOnto, gitClient, inputProvider, logger); + RebaseFromSourceBranch(branchToRebaseFrom, branchToRebaseOnto.Name, gitClient, inputProvider, logger); } } else if (lowestInactiveBranchToReParentFrom is null) { - lowestInactiveBranchToReParentFrom = branchToRebaseOnto; + lowestInactiveBranchToReParentFrom = branchToRebaseOnto.Name; } } } diff --git a/src/Stack/Config/Stack.cs b/src/Stack/Config/Stack.cs index 131c94a7..c4416f4c 100644 --- a/src/Stack/Config/Stack.cs +++ b/src/Stack/Config/Stack.cs @@ -1,4 +1,3 @@ -using System.Text.Json.Serialization; using Humanizer; namespace Stack.Config; @@ -12,6 +11,7 @@ public void SetPullRequestDescription(string description) this.PullRequestDescription = description; } + public List GetAllBranches() { var branchesToReturn = new List(); @@ -38,23 +38,7 @@ static List GetAllBranches(Branch branch) public List AllBranchNames => [.. GetAllBranches().Select(b => b.Name).Distinct()]; - public bool HasSingleTree - { - get - { - if (Branches.Count == 0) - { - return true; - } - - if (Branches.Count > 1) - { - return false; - } - - return Branches.First().HasSingleTree; - } - } + public bool HasSingleTree => GetAllBranchLines().Count == 1; public string GetDefaultBranchName() { @@ -109,6 +93,16 @@ static bool RemoveBranch(Branch branch, string branchName) return false; } + + public List> GetAllBranchLines() + { + var allLines = new List>(); + foreach (var branch in Branches) + { + allLines.AddRange(branch.GetAllPaths()); + } + return allLines; + } } public record Branch(string Name, List Children) @@ -126,21 +120,25 @@ public List AllBranchNames } } - public bool HasSingleTree + public List> GetAllPaths() { - get + var result = new List>(); + if (Children.Count == 0) { - if (Children.Count == 0) - { - return true; - } - - if (Children.Count > 1) + result.Add([this]); + } + else + { + foreach (var child in Children) { - return false; + foreach (var path in child.GetAllPaths()) + { + var newPath = new List { this }; + newPath.AddRange(path); + result.Add(newPath); + } } - - return Children.First().HasSingleTree; } + return result; } } \ No newline at end of file diff --git a/src/Stack/Git/GitClient.cs b/src/Stack/Git/GitClient.cs index effa7046..28e9277f 100644 --- a/src/Stack/Git/GitClient.cs +++ b/src/Stack/Git/GitClient.cs @@ -361,7 +361,7 @@ private string ExecuteGitCommandAndReturnOutput( } else { - throw new Exception("Failed to execute git command."); + throw new Exception($"Failed to execute git command: {command}. Exit code: {result}. Error: {errorBuilder}."); } }