Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 131 additions & 3 deletions src/Stack.Tests/Commands/Helpers/StackHelpersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand All @@ -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<IInputProvider>();
var gitClient = new GitClient(new TestLogger(testOutputHelper), repo.GitClientSettings);
var logger = new TestLogger(testOutputHelper);
var gitHubClient = Substitute.For<IGitHubClient>();

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<IInputProvider>();
var gitClient = new GitClient(new TestLogger(testOutputHelper), repo.GitClientSettings);
var logger = new TestLogger(testOutputHelper);
var gitHubClient = Substitute.For<IGitHubClient>();

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);
}
}
43 changes: 43 additions & 0 deletions src/Stack.Tests/Config/StackTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]>(
[
["A", "B", "C"],
["A", "B", "D"],
["A", "E"],
["A", "F", "G"]
], options => options.WithStrictOrdering());
}

[Fact]
public void GetDefaultBranchName_WhenNoBranchesInStack_ShouldReturnBranchNameWithTheNumber1AtTheEnd_BecauseItIsTheFirstBranch()
{
Expand Down
109 changes: 67 additions & 42 deletions src/Stack.Tests/Helpers/TestGitRepositoryBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text;
using LibGit2Sharp;
using Stack.Git;

Expand Down Expand Up @@ -84,13 +85,8 @@ public class CommitBuilder
{
Func<Repository, string>? 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)
{
Expand All @@ -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;
Expand All @@ -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<Blob>(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<LibGit2Sharp.Commit>() : 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;
}
}

Expand Down Expand Up @@ -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)
{
Expand All @@ -234,7 +261,6 @@ public TestGitRepositoryBuilder WithNumberOfEmptyCommits(Action<CommitBuilder> c
commitBuilders.Add(b =>
{
commitBuilder(b);
b.AllowEmptyCommit();
});
}
return this;
Expand All @@ -248,7 +274,6 @@ public TestGitRepositoryBuilder WithNumberOfEmptyCommitsOnRemoteTrackingBranchOf
{
commitBuilder(b);
b.OnBranch(r => r.Branches[branch].TrackedBranch.CanonicalName);
b.AllowEmptyCommit();
});
}
return this;
Expand Down
Loading