Skip to content

Commit

Permalink
Add git note for commits created from studio (#11693)
Browse files Browse the repository at this point in the history
* add git note on commit

* push notes

* git notes integration test

* Git note test for commit and push

* Fetch notes when clonning

* format

* after reset git note tests

* Fetch notes when in clone overload

* format fix

* add new test

* More tests

* add Integration tests trait
  • Loading branch information
mirkoSekulic authored Dec 6, 2023
1 parent 1617738 commit a93087b
Show file tree
Hide file tree
Showing 5 changed files with 359 additions and 101 deletions.
158 changes: 91 additions & 67 deletions backend/src/Designer/Services/Implementation/SourceControlSI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Commit = LibGit2Sharp.Commit;

namespace Altinn.Studio.Designer.Services.Implementation
{
Expand Down Expand Up @@ -56,23 +57,29 @@ public string CloneRemoteRepository(string org, string repository)
{
string remoteRepo = FindRemoteRepoLocation(org, repository);
CloneOptions cloneOptions = new();
cloneOptions.CredentialsProvider = (url, user, cred) => new UsernamePasswordCredentials { Username = GetAppToken(), Password = string.Empty };
return LibGit2Sharp.Repository.Clone(remoteRepo, FindLocalRepoLocation(org, repository), cloneOptions);
cloneOptions.CredentialsProvider = CredentialsProvider();
string localPath = FindLocalRepoLocation(org, repository);
string cloneResult = LibGit2Sharp.Repository.Clone(remoteRepo, localPath, cloneOptions);

FetchGitNotes(localPath);
return cloneResult;
}

/// <inheritdoc />
public string CloneRemoteRepository(string org, string repository, string destinationPath, string branchName = "")
{
string remoteRepo = FindRemoteRepoLocation(org, repository);
CloneOptions cloneOptions = new();
cloneOptions.CredentialsProvider = (url, user, cred) => new UsernamePasswordCredentials { Username = GetAppToken(), Password = string.Empty };
cloneOptions.CredentialsProvider = CredentialsProvider();

if (!string.IsNullOrEmpty(branchName))
{
cloneOptions.BranchName = branchName;
}

return LibGit2Sharp.Repository.Clone(remoteRepo, destinationPath, cloneOptions);
string cloneResult = LibGit2Sharp.Repository.Clone(remoteRepo, destinationPath, cloneOptions);
FetchGitNotes(destinationPath);
return cloneResult;
}

/// <inheritdoc />
Expand All @@ -89,8 +96,7 @@ public RepoStatus PullRemoteChanges(string org, string repository)
},
};
pullOptions.FetchOptions = new FetchOptions();
pullOptions.FetchOptions.CredentialsProvider = (url, user, cred) =>
new UsernamePasswordCredentials { Username = GetAppToken(), Password = string.Empty };
pullOptions.FetchOptions.CredentialsProvider = CredentialsProvider();

try
{
Expand Down Expand Up @@ -130,8 +136,7 @@ public void FetchRemoteChanges(string org, string repository)
using (var repo = new LibGit2Sharp.Repository(FindLocalRepoLocation(org, repository)))
{
FetchOptions fetchOptions = new();
fetchOptions.CredentialsProvider = (url, user, cred) =>
new UsernamePasswordCredentials { Username = GetAppToken(), Password = string.Empty };
fetchOptions.CredentialsProvider = CredentialsProvider();

foreach (Remote remote in repo?.Network?.Remotes)
{
Expand Down Expand Up @@ -166,31 +171,29 @@ public async Task<bool> Push(string org, string repository)
{
bool pushSuccess = true;
string localServiceRepoFolder = _settings.GetServicePath(org, repository, AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext));
using (LibGit2Sharp.Repository repo = new(localServiceRepoFolder))
using LibGit2Sharp.Repository repo = new(localServiceRepoFolder);
string remoteUrl = FindRemoteRepoLocation(org, repository);
Remote remote = repo.Network.Remotes["origin"];

if (!remote.PushUrl.Equals(remoteUrl))
{
string remoteUrl = FindRemoteRepoLocation(org, repository);
Remote remote = repo.Network.Remotes["origin"];
// This is relevant when we switch beteen running designer in local or in docker. The remote URL changes.
// Requires adminstrator access to update files.
repo.Network.Remotes.Update("origin", r => r.Url = remoteUrl);
}

if (!remote.PushUrl.Equals(remoteUrl))
PushOptions options = new()
{
OnPushStatusError = pushError =>
{
// This is relevant when we switch beteen running designer in local or in docker. The remote URL changes.
// Requires adminstrator access to update files.
repo.Network.Remotes.Update("origin", r => r.Url = remoteUrl);
_logger.LogError("Push error: {0}", pushError.Message);
pushSuccess = false;
}
};
options.CredentialsProvider = CredentialsProvider();

PushOptions options = new()
{
OnPushStatusError = pushError =>
{
_logger.LogError("Push error: {0}", pushError.Message);
pushSuccess = false;
}
};
options.CredentialsProvider = (url, user, cred) =>
new UsernamePasswordCredentials { Username = GetAppToken(), Password = string.Empty };

repo.Network.Push(remote, @"refs/heads/master", options);
}
repo.Network.Push(remote, @"refs/heads/master", options);
repo.Network.Push(remote, "refs/notes/commits", options);

return await Task.FromResult(pushSuccess);
}
Expand All @@ -201,24 +204,31 @@ public async Task<bool> Push(string org, string repository)
/// <param name="commitInfo">Information about the commit</param>
public void Commit(CommitInfo commitInfo)
{
string localServiceRepoFolder = _settings.GetServicePath(commitInfo.Org, commitInfo.Repository, AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext));
using (LibGit2Sharp.Repository repo = new(localServiceRepoFolder))
CommitAndAddStudioNote(commitInfo.Org, commitInfo.Repository, commitInfo.Message);
}

private void CommitAndAddStudioNote(string org, string repository, string message)
{
string localServiceRepoFolder = _settings.GetServicePath(org, repository, AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext));
using LibGit2Sharp.Repository repo = new LibGit2Sharp.Repository(localServiceRepoFolder);
string remoteUrl = FindRemoteRepoLocation(org, repository);
Remote remote = repo.Network.Remotes["origin"];

if (!remote.PushUrl.Equals(remoteUrl))
{
string remoteUrl = FindRemoteRepoLocation(commitInfo.Org, commitInfo.Repository);
Remote remote = repo.Network.Remotes["origin"];
// This is relevant when we switch beteen running designer in local or in docker. The remote URL changes.
// Requires adminstrator access to update files.
repo.Network.Remotes.Update("origin", r => r.Url = remoteUrl);
}

if (!remote.PushUrl.Equals(remoteUrl))
{
// This is relevant when we switch beteen running designer in local or in docker. The remote URL changes.
// Requires adminstrator access to update files.
repo.Network.Remotes.Update("origin", r => r.Url = remoteUrl);
}
Commands.Stage(repo, "*");

Commands.Stage(repo, "*");
LibGit2Sharp.Signature signature = GetDeveloperSignature();
var commit = repo.Commit(message, signature, signature);

var notes = repo.Notes;
notes.Add(commit.Id, "studio-commit", signature, signature, notes.DefaultNamespace);

LibGit2Sharp.Signature signature = GetDeveloperSignature();
repo.Commit(commitInfo.Message, signature, signature);
}
}

/// <summary>
Expand Down Expand Up @@ -463,39 +473,41 @@ public void VerifyCloneExists(string org, string repository)

private void CommitAndPushToBranch(string org, string repository, string branchName, string localPath, string message)
{
using (LibGit2Sharp.Repository repo = new(localPath))
using LibGit2Sharp.Repository repo = new(localPath);
// Restrict users from empty commit
if (repo.RetrieveStatus().IsDirty)
{
// Restrict users from empty commit
if (repo.RetrieveStatus().IsDirty)
{
string remoteUrl = FindRemoteRepoLocation(org, repository);
Remote remote = repo.Network.Remotes["origin"];
string remoteUrl = FindRemoteRepoLocation(org, repository);
Remote remote = repo.Network.Remotes["origin"];

if (!remote.PushUrl.Equals(remoteUrl))
{
// This is relevant when we switch beteen running designer in local or in docker. The remote URL changes.
// Requires adminstrator access to update files.
repo.Network.Remotes.Update("origin", r => r.Url = remoteUrl);
}
if (!remote.PushUrl.Equals(remoteUrl))
{
// This is relevant when we switch beteen running designer in local or in docker. The remote URL changes.
// Requires adminstrator access to update files.
repo.Network.Remotes.Update("origin", r => r.Url = remoteUrl);
}

Commands.Stage(repo, "*");
Commands.Stage(repo, "*");

LibGit2Sharp.Signature signature = GetDeveloperSignature();
repo.Commit(message, signature, signature);
LibGit2Sharp.Signature signature = GetDeveloperSignature();
var commit = repo.Commit(message, signature, signature);
var notes = repo.Notes;
notes.Add(commit.Id, "studio-commit", signature, signature, notes.DefaultNamespace);

PushOptions options = new();
options.CredentialsProvider = (url, user, cred) =>
new UsernamePasswordCredentials { Username = GetAppToken(), Password = string.Empty };
PushOptions options = new();
options.CredentialsProvider = CredentialsProvider();

if (branchName == "master")
{
repo.Network.Push(remote, @"refs/heads/master", options);
return;
}
if (branchName == "master")
{
repo.Network.Push(remote, @"refs/heads/master", options);
repo.Network.Push(remote, "refs/notes/commits", options);

Branch b = repo.Branches[branchName];
repo.Network.Push(b, options);
return;
}

Branch b = repo.Branches[branchName];
repo.Network.Push(b, options);
repo.Network.Push(remote, "refs/notes/commits", options);
}
}

Expand Down Expand Up @@ -624,5 +636,17 @@ private LibGit2Sharp.Signature GetDeveloperSignature()
{
return new LibGit2Sharp.Signature(AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext), "@jugglingnutcase", DateTime.Now);
}

private LibGit2Sharp.Handlers.CredentialsHandler CredentialsProvider() => (url, user, cred) => new UsernamePasswordCredentials { Username = GetAppToken(), Password = string.Empty };

private void FetchGitNotes(string localRepositoryPath)
{
using var repo = new LibGit2Sharp.Repository(localRepositoryPath);
var options = new FetchOptions()
{
CredentialsProvider = CredentialsProvider()
};
Commands.Fetch(repo, "origin", new List<string> { "refs/notes/*:refs/notes/*" }, options, "fetch notes");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using Designer.Tests.Fixtures;
using DotNet.Testcontainers.Builders;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.Mvc.Testing.Handlers;
using Microsoft.AspNetCore.TestHost;
Expand All @@ -28,6 +27,10 @@ public abstract class GiteaIntegrationTestsBase<TControllerTest> : ApiTestsBase<

private CookieContainer CookieContainer { get; } = new CookieContainer();

/// On some systems path too long error occurs if repo is nested deep in file system.
protected override string TestRepositoriesLocation =>
Path.Combine(Path.GetTempPath(), "altinn", "tests", "repos");

/// <summary>
/// Used when performing chained calls to designer api
/// </summary>
Expand All @@ -50,14 +53,17 @@ protected override void Dispose(bool disposing)
DeleteDirectoryIfExists(CreatedFolderPath);
}

private static void DeleteDirectoryIfExists(string directoryPath)
protected static void DeleteDirectoryIfExists(string directoryPath)
{
if (string.IsNullOrWhiteSpace(directoryPath) || !Directory.Exists(directoryPath))
{
return;
}

var directory = new DirectoryInfo(directoryPath) { Attributes = FileAttributes.Normal };
var directory = new DirectoryInfo(directoryPath)
{
Attributes = FileAttributes.Normal
};

foreach (var info in directory.GetFileSystemInfos("*", SearchOption.AllDirectories))
{
Expand Down Expand Up @@ -131,5 +137,31 @@ protected async Task CreateAppUsingDesigner(string org, string repoName)
response.StatusCode.Should().Be(HttpStatusCode.Created);
InvalidateAllCookies();
}

protected static string GetCommitInfoJson(string text, string org, string repository) =>
@$"{{
""message"": ""{text}"",
""org"": ""{org}"",
""repository"": ""{repository}""
}}";

protected static string GenerateCommitJsonPayload(string text, string message) =>
@$"{{
""author"": {{
""email"": ""{GiteaConstants.AdminEmail}"",
""name"": ""{GiteaConstants.AdminUser}""
}},
""committer"": {{
""email"": ""{GiteaConstants.AdminEmail}"",
""name"": ""{GiteaConstants.AdminUser}""
}},
""content"": ""{Convert.ToBase64String(Encoding.UTF8.GetBytes(text))}"",
""dates"": {{
""author"": ""{DateTime.Now:O}"",
""committer"": ""{DateTime.Now:O}""
}},
""message"": ""{message}"",
""signoff"": true
}}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Designer.Tests.Fixtures;
using Designer.Tests.Utils;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using Polly;
using Polly.Retry;
using Xunit;

namespace Designer.Tests.GiteaIntegrationTests.RepositoryController
{
public class CopyAppGiteaIntegrationTests : GiteaIntegrationTestsBase<CopyAppGiteaIntegrationTests>, IClassFixture<WebApplicationFactory<Program>>
{

private string CopyRepoName { get; set; }

public CopyAppGiteaIntegrationTests(WebApplicationFactory<Program> factory, GiteaFixture giteaFixture) : base(factory, giteaFixture)
{
}

[Theory]
[Trait("Category", "GiteaIntegrationTest")]
[InlineData(GiteaConstants.TestOrgUsername)]
public async Task Copy_Repo_Should_Return_Created(string org)
{
string targetRepo = TestDataHelper.GenerateTestRepoName("-gitea");
await CreateAppUsingDesigner(org, targetRepo);

CopyRepoName = TestDataHelper.GenerateTestRepoName("-gitea-copy");

// Copy app
using HttpResponseMessage commitResponse = await HttpClient.PostAsync($"designer/api/repos/repo/{org}/copy-app?sourceRepository={targetRepo}&targetRepository={CopyRepoName}", null);
commitResponse.StatusCode.Should().Be(HttpStatusCode.Created);
}

protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
{
return;
}
if (string.IsNullOrEmpty(CopyRepoName))
{
return;
}

string copyRepoPath = Path.Combine(TestRepositoriesLocation, "testUser", "ttd", CopyRepoName);
DeleteDirectoryIfExists(copyRepoPath);
}
}
}
Loading

0 comments on commit a93087b

Please sign in to comment.