diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index c06f8aa0..126835bd 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -59,12 +59,10 @@ updates:
exclude-patterns:
- "*Analyzer*"
- "Microsoft.NET.Test.Sdk"
- - "Microsoft.Playwright"
test-dependencies:
patterns:
- "coverlet.collector"
- "Microsoft.NET.Test.Sdk"
- - "Microsoft.Playwright"
- "NSubstitute"
- "*NUnit*"
exclude-patterns:
@@ -78,6 +76,5 @@ updates:
- "System.*"
- "coverlet.collector"
- "Microsoft.NET.Test.Sdk"
- - "Microsoft.Playwright"
- "NSubstitute"
- "*NUnit*"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a81b4cb1..5dc13ea0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -57,8 +57,8 @@ jobs:
gitlab:
# Keep in sync with the version in GitLabDockerContainer.cs
# Available tags: https://hub.docker.com/r/gitlab/gitlab-ee/tags?name=-ee.0
- - "gitlab/gitlab-ee:17.1.8-ee.0"
- "gitlab/gitlab-ee:18.1.6-ee.0"
+ - "gitlab/gitlab-ee:19.3.1-ee.0"
configuration: [Release]
fail-fast: false
services:
diff --git a/Directory.Packages.props b/Directory.Packages.props
index d3ce786c..ef381efb 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -13,7 +13,6 @@
-
diff --git a/NGitLab.Tests/CommitsTests.cs b/NGitLab.Tests/CommitsTests.cs
index 01440a79..ed100a4d 100644
--- a/NGitLab.Tests/CommitsTests.cs
+++ b/NGitLab.Tests/CommitsTests.cs
@@ -4,6 +4,7 @@
using System.Threading.Tasks;
using NGitLab.Models;
using NGitLab.Tests.Docker;
+using NuGet.Versioning;
using NUnit.Framework;
namespace NGitLab.Tests;
@@ -70,9 +71,13 @@ public async Task Test_can_get_stats_in_commit()
[Test]
[NGitLabRetry]
- public async Task Test_can_get_merge_request_associated_to_commit()
+ [TestCase("(,19.0.0)", null)] // v18 allowed empty commits by default
+ [TestCase("[19.0.0,)", true)] // v19 only allow empty commits if specified
+ public async Task Test_can_get_merge_request_associated_to_commit(string versionRange, bool? allowEmpty)
{
using var context = await GitLabTestContext.CreateAsync();
+ context.IgnoreTestIfGitLabVersionOutOfRange(VersionRange.Parse(versionRange));
+
var project = context.CreateProject();
context.Client.GetRepository(project.Id).Branches.Create(new BranchCreate { Name = "test-mr", Ref = project.DefaultBranch });
@@ -81,6 +86,7 @@ public async Task Test_can_get_merge_request_associated_to_commit()
{
Branch = "test-mr",
CommitMessage = "Test to retrieve MR from commit sha",
+ AllowEmpty = allowEmpty,
});
var mergeRequestTitle = "Test to retrieve MR from commit sha";
diff --git a/NGitLab.Tests/ContributorsTests.cs b/NGitLab.Tests/ContributorsTests.cs
index fcc5dc48..4db053cc 100644
--- a/NGitLab.Tests/ContributorsTests.cs
+++ b/NGitLab.Tests/ContributorsTests.cs
@@ -3,6 +3,7 @@
using System.Threading.Tasks;
using NGitLab.Models;
using NGitLab.Tests.Docker;
+using NuGet.Versioning;
using NUnit.Framework;
namespace NGitLab.Tests;
@@ -25,9 +26,14 @@ public async Task Test_can_get_contributors()
[Test]
[NGitLabRetry]
- public async Task Test_can_get_MultipleContributors()
+
+ [TestCase("(,19.0.0)", null)] // v18 allowed empty commits by default
+ [TestCase("[19.0.0,)", true)] // v19 only allow empty commits if specified
+ public async Task Test_can_get_MultipleContributors(string versionRange, bool? allowEmpty)
{
using var context = await GitLabTestContext.CreateAsync();
+ context.IgnoreTestIfGitLabVersionOutOfRange(VersionRange.Parse(versionRange));
+
var project = context.CreateProject(initializeWithCommits: true);
var contributorsClient = context.Client.GetRepository(project.Id).Contributors;
var currentUser = context.Client.Users.Current;
@@ -59,6 +65,7 @@ public async Task Test_can_get_MultipleContributors()
Branch = project.DefaultBranch,
StartBranch = project.DefaultBranch,
CommitMessage = "test",
+ AllowEmpty = allowEmpty,
});
var contributors = await GitLabTestContext.RetryUntilAsync(() => contributorsClient.All.ToList(), c => c.Count >= 2, TimeSpan.FromMinutes(2));
diff --git a/NGitLab.Tests/Docker/GitLabCredential.cs b/NGitLab.Tests/Docker/GitLabCredential.cs
index 9e9d743d..a9444c2d 100644
--- a/NGitLab.Tests/Docker/GitLabCredential.cs
+++ b/NGitLab.Tests/Docker/GitLabCredential.cs
@@ -1,12 +1,8 @@
-namespace NGitLab.Tests.Docker;
+namespace NGitLab.Tests.Docker;
public record GitLabCredential
{
public string AdminUserToken { get; set; }
- public string AdminCookies { get; set; }
-
public string UserToken { get; set; }
-
- public string ProfileToken { get; set; }
}
diff --git a/NGitLab.Tests/Docker/GitLabDockerContainer.cs b/NGitLab.Tests/Docker/GitLabDockerContainer.cs
index 54290949..cd3a455f 100644
--- a/NGitLab.Tests/Docker/GitLabDockerContainer.cs
+++ b/NGitLab.Tests/Docker/GitLabDockerContainer.cs
@@ -13,9 +13,7 @@
using System.Threading.Tasks;
using Docker.DotNet;
using Docker.DotNet.Models;
-using Microsoft.Playwright;
using NGitLab.Models;
-using NuGet.Versioning;
using NUnit.Framework;
using Polly;
@@ -34,12 +32,7 @@ public class GitLabDockerContainer
/// Keep in sync with .github/workflows/ci.yml, use the lowest supported version
/// List of available versions: https://hub.docker.com/r/gitlab/gitlab-ee/tags/
///
- private const string LocalGitLabDockerVersion = "18.1.6-ee.0";
-
- ///
- /// Resolved GitLab version taken from the help page once logged in
- ///
- private static string ResolvedGitLabVersion;
+ private const string LocalGitLabDockerVersion = "19.3.1-ee.0";
private static string s_creationErrorMessage;
private static readonly SemaphoreSlim s_setupLock = new(initialCount: 1, maxCount: 1);
@@ -113,21 +106,7 @@ private async Task SetupAsync()
await SpawnDockerContainerAsync().ConfigureAwait(false);
}
- EnsureChromiumIsInstalled();
-
- // Use Playwright to launch Chromium
- using var playwright = await Playwright.CreateAsync();
- await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
- {
- // Headless = false, // Uncomment to have browser window visible
- // SlowMo = 1000, // Slows down Playwright operations by the specified amount of ms.
- });
- await using var browserContext = await browser.NewContextAsync();
-
- await LoginAsync(browserContext);
- await ResolveGitLabVersionAsync(browserContext).ConfigureAwait(false);
-
- await LoadCredentialsAsync().ConfigureAwait(false);
+ LoadCredentials();
if (Credentials != null)
{
@@ -135,19 +114,8 @@ private async Task SetupAsync()
return;
}
- await GenerateCredentialsAsync(browserContext).ConfigureAwait(false);
+ await GenerateCredentialsAsync().ConfigureAwait(false);
PersistCredentialsAsync();
-
- static void EnsureChromiumIsInstalled()
- {
- TestContext.Progress.WriteLine("Making sure Chromium is installed");
-
- var exitCode = Microsoft.Playwright.Program.Main(new[] { "install", "--force", "chromium", "--with-deps" });
- if (exitCode != 0)
- throw new InvalidOperationException($"Cannot install browser (exit code: {exitCode})");
-
- TestContext.Progress.WriteLine("Chromium installed");
- }
}
private static async Task ValidateDockerIsEnabled(DockerClient client)
@@ -316,7 +284,7 @@ private async Task SpawnDockerContainerAsync()
TestContext.Progress.WriteLine("GitLab Docker container is ready");
}
- private async Task GenerateCredentialsAsync(IBrowserContext browserContext)
+ private async Task GenerateCredentialsAsync()
{
Console.WriteLine("Requesting credentials from GitLab instance");
@@ -333,84 +301,31 @@ async Task GenerateAdminToken(GitLabCredential credentials)
{
TestContext.Progress.WriteLine("Generating Credentials");
- var gitLabVersionAsNuGetVersion = NuGetVersion.Parse(ResolvedGitLabVersion);
- var isMajorVersion15 = VersionRange.Parse("[15.0,16.0)").Satisfies(gitLabVersionAsNuGetVersion);
- var isMajorVersionAtLeast16 = VersionRange.Parse("[16.0,)").Satisfies(gitLabVersionAsNuGetVersion);
- var isMajorVersionAtLeast18 = VersionRange.Parse("[18.0,)").Satisfies(gitLabVersionAsNuGetVersion);
-
- TestContext.Progress.WriteLine("Creating root token");
-
- var accessTokenRelativeUri = "/-/profile/personal_access_tokens";
- if (isMajorVersionAtLeast18)
- {
- accessTokenRelativeUri = "/-/user_settings/personal_access_tokens";
- }
-
- var page = await browserContext.NewPageAsync();
- await page.GotoAsync(new Uri(GitLabUrl, accessTokenRelativeUri).ToString());
-
- var formLocator = page.Locator("main#content-body form");
-
- var tokenName = "GitLabClientTest-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture);
-
- if (isMajorVersionAtLeast18)
- {
- await page.Locator("main[id='content-body'] button[data-testid='add-new-token-button']").ClickAsync(new LocatorClickOptions { Timeout = 5_000 });
- formLocator = page.Locator("form[id='token-create-form']");
- await formLocator.Locator("input[data-testid='access-token-name-field']").FillAsync(tokenName);
- }
- else if (isMajorVersionAtLeast16)
- {
- await SkipVersionReminder(page);
-
- await page.Locator("main[id='content-body'] button[data-testid='add-new-token-button']").ClickAsync(new LocatorClickOptions { Timeout = 5_000 });
- formLocator = page.Locator("main[id='content-body'] form[id='js-new-access-token-form']");
- await formLocator.Locator("input[data-testid='access-token-name-field']").FillAsync(tokenName);
- }
- else if (isMajorVersion15)
- {
- // Try the "old" 15.x.y way
- formLocator = page.Locator("main#content-body form");
- await formLocator.GetByLabel("Token name").FillAsync(tokenName);
- }
- else
- {
- s_creationErrorMessage = $"Unable to generate an admin token: resolved GitLab version '{ResolvedGitLabVersion}' doesn't match any supported range in '{nameof(GenerateCredentialsAsync)}'.";
- Assert.Fail(s_creationErrorMessage);
- }
-
- foreach (var checkbox in await formLocator.GetByRole(AriaRole.Checkbox).AllAsync())
- {
- await checkbox.CheckAsync(new LocatorCheckOptions { Force = true });
- }
-
- string token = null;
- if (isMajorVersionAtLeast18)
- {
- await formLocator.GetByTestId("create-token-button").ClickAsync();
- await page.GetByRole(AriaRole.Alert).GetByLabel("Click to reveal").ClickAsync();
- token = await page.GetByTestId("created-access-token-field").InputValueAsync();
- }
- else
+ using var conf = new DockerClientConfiguration(new Uri(OperatingSystem.IsWindows() ? "npipe://./pipe/docker_engine" : "unix:///var/run/docker.sock"));
+ using var client = conf.CreateClient();
+ await ValidateDockerIsEnabled(client).ConfigureAwait(false);
+
+ TestContext.Progress.WriteLine("Creating root token via 'gitlab-rails runner'");
+
+ // Keep only scopes the running GitLab version supports (an unknown scope makes `create!` raise).
+ const string script = """
+ desired_scopes = %w[api read_user read_api read_repository write_repository sudo admin_mode create_runner manage_runner k8s_proxy]
+ available_scopes = Gitlab::Auth.all_available_scopes.map(&:to_s)
+ token = User.find_by_username!('root').personal_access_tokens.create!(
+ name: 'NGitLabClientTest',
+ scopes: (desired_scopes & available_scopes),
+ expires_at: 1.year.from_now)
+ puts token.token
+ """;
+
+ var retryPolicy = Policy.Handle().WaitAndRetryAsync(20, _ => TimeSpan.FromSeconds(3));
+ var token = await retryPolicy.ExecuteAsync(async () =>
{
- await formLocator.GetByRole(AriaRole.Button, new() { Name = "Create personal access token" }).ClickAsync();
- token = await page.Locator("button[title='Copy personal access token']").GetAttributeAsync("data-clipboard-text");
- }
+ var containerId = await ResolveGitLabContainerIdAsync(client).ConfigureAwait(false);
+ return await RunGitLabRailsRunnerAsync(client, containerId, script).ConfigureAwait(false);
+ }).ConfigureAwait(false);
credentials.AdminUserToken = token;
-
- // Get admin login cookie
- // result.Cookie: experimentation_subject_id=XXX; _gitlab_session=XXXX; known_sign_in=XXXX
- TestContext.Progress.WriteLine("Extracting GitLab session cookie");
- var cookies = await browserContext.CookiesAsync(new[] { GitLabUrl.AbsoluteUri });
- foreach (var cookie in cookies)
- {
- if (cookie.Name == "_gitlab_session")
- {
- credentials.AdminCookies = cookie.Value;
- break;
- }
- }
}
void GenerateUserToken()
@@ -454,15 +369,48 @@ void GenerateUserToken()
}
}
- private static async Task SkipVersionReminder(IPage page)
+ private static async Task ResolveGitLabContainerIdAsync(DockerClient client)
{
- try
+ var containers = await client.Containers.ListContainersAsync(new ContainersListParameters { All = true }).ConfigureAwait(false);
+
+ var container = containers.FirstOrDefault(c => c.Names.Contains("/" + ContainerName, StringComparer.Ordinal))
+ ?? containers.FirstOrDefault(c => c.Image.StartsWith(ImageName, StringComparison.Ordinal));
+
+ if (container == null)
+ throw new InvalidOperationException($"Cannot find a running Docker container for image '{ImageName}' to generate credentials from.");
+
+ return container.ID;
+ }
+
+ private static async Task RunGitLabRailsRunnerAsync(DockerClient client, string containerId, string script)
+ {
+ var execCreateResponse = await client.Exec.ExecCreateContainerAsync(containerId, new ContainerExecCreateParameters
{
- await page.Locator("button[data-testid='alert-modal-remind-button']").ClickAsync(new LocatorClickOptions { Timeout = 3_000 });
- }
- catch (Exception)
+ AttachStdout = true,
+ AttachStderr = true,
+ Cmd = ["gitlab-rails", "runner", script],
+ }).ConfigureAwait(false);
+
+ string stdout;
+ string stderr;
+ using (var stream = await client.Exec.StartAndAttachContainerExecAsync(execCreateResponse.ID, tty: false).ConfigureAwait(false))
{
+ (stdout, stderr) = await stream.ReadOutputToEndAsync(CancellationToken.None).ConfigureAwait(false);
}
+
+ var inspectResponse = await client.Exec.InspectContainerExecAsync(execCreateResponse.ID).ConfigureAwait(false);
+ if (inspectResponse.ExitCode != 0)
+ throw new InvalidOperationException($"'gitlab-rails runner' failed with exit code {inspectResponse.ExitCode}.\nStdout: {stdout}\nStderr: {stderr}");
+
+ var token = stdout
+ .Split('\n')
+ .Select(line => line.Trim())
+ .LastOrDefault(line => line.Length > 0);
+
+ if (string.IsNullOrEmpty(token))
+ throw new InvalidOperationException($"'gitlab-rails runner' did not output a token.\nStdout: {stdout}\nStderr: {stderr}");
+
+ return token;
}
private void PersistCredentialsAsync()
@@ -473,40 +421,26 @@ private void PersistCredentialsAsync()
File.WriteAllText(path, json);
}
- private async Task LoadCredentialsAsync()
+ private void LoadCredentials()
{
var file = GetCredentialsFilePath();
- if (File.Exists(file))
- {
- var json = File.ReadAllText(file);
- var credentials = JsonSerializer.Deserialize(json);
- if (credentials.AdminUserToken == null || credentials.UserToken == null)
- return;
+ if (!File.Exists(file))
+ return;
- var client = new GitLabClient(GitLabUrl.ToString(), credentials.AdminUserToken);
- try
- {
- // Validate token
- var user = client.Users.Current;
+ var json = File.ReadAllText(file);
+ var credentials = JsonSerializer.Deserialize(json);
+ if (credentials.AdminUserToken == null || credentials.UserToken == null)
+ return;
- using var httpClient = new HttpClient
- {
- BaseAddress = GitLabUrl,
- DefaultRequestHeaders =
- {
- { "Cookie", "_gitlab_session=" + credentials.AdminCookies },
- },
- };
- var response = await httpClient.GetAsync(new Uri("/", UriKind.RelativeOrAbsolute));
- if (response.RequestMessage.RequestUri.PathAndQuery == "/users/sign_in")
- return;
-
- // Validate cookie
- Credentials = credentials;
- }
- catch (GitLabException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
- {
- }
+ var client = new GitLabClient(GitLabUrl.ToString(), credentials.AdminUserToken);
+ try
+ {
+ // Validate token
+ _ = client.Users.Current;
+ Credentials = credentials;
+ }
+ catch (GitLabException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
+ {
}
}
@@ -540,84 +474,4 @@ private async Task WaitForCiGitLabInstance()
s_creationErrorMessage = "GitLab is not well configured in CI";
Assert.Fail(s_creationErrorMessage);
}
-
- private async Task ResolveGitLabVersionAsync(IBrowserContext browserContext)
- {
- Console.WriteLine("Resolving GitLab version from help page...");
- var page = await browserContext.NewPageAsync();
- await page.GotoAsync(new Uri(GitLabUrl, "help").AbsoluteUri);
- var titleLink = await page.QuerySelectorAsync("h1 a");
-
- if (titleLink is null)
- {
- s_creationErrorMessage = "Cannot find title on the help page to get GitLab version";
- Assert.Fail(s_creationErrorMessage);
- }
-
- var version = await titleLink.TextContentAsync();
-
- if (string.IsNullOrEmpty(version))
- {
- s_creationErrorMessage = "Found title on the help page, but the version is empty";
- Assert.Fail(s_creationErrorMessage);
- }
-
- ResolvedGitLabVersion = version.Trim().TrimStart('v');
- Console.WriteLine($"GitLab resolved version is '{ResolvedGitLabVersion}'");
-
- await CloseRedesignModal(page);
- }
-
- private async Task LoginAsync(IBrowserContext browserContext)
- {
- var page = await browserContext.NewPageAsync();
- await page.GotoAsync(GitLabUrl.AbsoluteUri);
- var url = await GetCurrentUrl(page);
-
- if (url != "/users/sign_in")
- {
- Console.WriteLine("Already logged in on GitLab instance");
- return;
- }
-
- Console.WriteLine("Logging in on GitLab instance...");
-
- var v15LoginInput = "form#new_user input[name='user[login]']";
- var v16LoginInput = "form[data-testid='sign-in-form'] input[name='user[login]']";
-
- if (await page.QuerySelectorAsync(v15LoginInput) is not null)
- {
- await page.Locator(v15LoginInput).FillAsync(AdminUserName);
- await page.Locator("form#new_user input[name='user[password]']").FillAsync(AdminPassword);
- }
- else if (await page.QuerySelectorAsync(v16LoginInput) is not null)
- {
- await page.Locator(v16LoginInput).FillAsync(AdminUserName);
- await page.Locator("form[data-testid='sign-in-form'] input[name='user[password]']").FillAsync(AdminPassword);
- }
- else
- {
- s_creationErrorMessage = $"Unable to find the correct login input. Please make sure that login form for the GitLab version you target is supported in '{nameof(LoginAsync)}'";
- Assert.Fail(s_creationErrorMessage);
- }
-
- var checkbox = page.Locator("form[data-testid='sign-in-form'] input[type=checkbox][name='user[remember_me]']");
- await checkbox.CheckAsync(new LocatorCheckOptions { Force = true });
-
- await page.RunAndWaitForResponseAsync(async () =>
- {
- await page.EvalOnSelectorAsync("form[data-testid='sign-in-form']", "form => form.submit()");
- }, response => response.Status == 200);
- }
-
- private async Task CloseRedesignModal(IPage page)
- {
- var isModalVisible = await page.IsVisibleAsync("div#dap_welcome_modal button[aria-label='Close']");
- if (isModalVisible)
- {
- await page.Locator("div#dap_welcome_modal button[aria-label='Close']").ClickAsync();
- }
- }
-
- private static Task GetCurrentUrl(IPage page) => page.EvaluateAsync("window.location.pathname");
}
diff --git a/NGitLab.Tests/Docker/GitLabTestContext.cs b/NGitLab.Tests/Docker/GitLabTestContext.cs
index 0bb96a13..7472a12a 100644
--- a/NGitLab.Tests/Docker/GitLabTestContext.cs
+++ b/NGitLab.Tests/Docker/GitLabTestContext.cs
@@ -52,7 +52,7 @@ private GitLabTestContext(GitLabDockerContainer container)
BaseAddress = DockerContainer.GitLabUrl,
DefaultRequestHeaders =
{
- { "Cookie", "_gitlab_session=" + DockerContainer.Credentials.AdminCookies },
+ { "Authorization", "Bearer " + DockerContainer.Credentials.AdminUserToken },
},
};
}
diff --git a/NGitLab.Tests/NGitLab.Tests.csproj b/NGitLab.Tests/NGitLab.Tests.csproj
index 3aa6838a..4e353f38 100644
--- a/NGitLab.Tests/NGitLab.Tests.csproj
+++ b/NGitLab.Tests/NGitLab.Tests.csproj
@@ -9,7 +9,6 @@
-
diff --git a/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs b/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs
index e6536ef1..4a060127 100644
--- a/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs
+++ b/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs
@@ -474,17 +474,18 @@ public async Task GetArchive_QuerySpecifiesPath_PathValuePassedCorrectly()
[Test]
[NGitLabRetry]
- public async Task GetArchive_QuerySpecifiesAllParameters_AllParametersPassedCorrectly()
+ [TestCase("", 0)] // First commit creates a file on root
+ [TestCase(RepositoryClientTestsContext.SubfolderName, 1)] // Second commit creates a file on a subfolder
+ public async Task GetArchive_QuerySpecifiesAllParameters_AllParametersPassedCorrectly(string path, int commitIndex)
{
// Arrange
using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2);
- var firstCommitId = context.Commits[0].Id.ToString();
- var path = RepositoryClientTestsContext.SubfolderName;
+ var commit = context.Commits[commitIndex].Id.ToString();
var fileArchiveQuery = new FileArchiveQuery
{
Format = FileArchiveFormat.Zip,
Path = path,
- Ref = firstCommitId,
+ Ref = commit,
};
// Act
@@ -498,7 +499,7 @@ public async Task GetArchive_QuerySpecifiesAllParameters_AllParametersPassedCorr
Assert.That(requestPathAndQuery, Is.Not.Null);
Assert.That(requestPathAndQuery.Contains($"/archive.zip", StringComparison.OrdinalIgnoreCase), Is.True);
Assert.That(requestPathAndQuery.Contains($"path={path}", StringComparison.OrdinalIgnoreCase), Is.True);
- Assert.That(requestPathAndQuery.Contains($"sha={firstCommitId}", StringComparison.OrdinalIgnoreCase), Is.True);
+ Assert.That(requestPathAndQuery.Contains($"sha={commit}", StringComparison.OrdinalIgnoreCase), Is.True);
}
}
diff --git a/NGitLab/Models/CommitCreate.cs b/NGitLab/Models/CommitCreate.cs
index ee79c061..54c62d50 100644
--- a/NGitLab/Models/CommitCreate.cs
+++ b/NGitLab/Models/CommitCreate.cs
@@ -30,6 +30,9 @@ public class CommitCreate
[JsonPropertyName("force")]
public bool? Force { get; set; }
+
+ [JsonPropertyName("allow_empty")]
+ public bool? AllowEmpty { get; set; }
}
public class CreateCommitAction
diff --git a/NGitLab/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/NGitLab/PublicAPI/net10.0/PublicAPI.Unshipped.txt
index 8bfcb4d7..398a4b86 100644
--- a/NGitLab/PublicAPI/net10.0/PublicAPI.Unshipped.txt
+++ b/NGitLab/PublicAPI/net10.0/PublicAPI.Unshipped.txt
@@ -1564,6 +1564,8 @@ NGitLab.Models.CommitCherryPick.Sha.set -> void
NGitLab.Models.CommitCreate
NGitLab.Models.CommitCreate.Actions.get -> System.Collections.Generic.IList
NGitLab.Models.CommitCreate.Actions.set -> void
+NGitLab.Models.CommitCreate.AllowEmpty.get -> bool?
+NGitLab.Models.CommitCreate.AllowEmpty.set -> void
NGitLab.Models.CommitCreate.AuthorEmail.get -> string
NGitLab.Models.CommitCreate.AuthorEmail.set -> void
NGitLab.Models.CommitCreate.AuthorName.get -> string
diff --git a/NGitLab/PublicAPI/net472/PublicAPI.Unshipped.txt b/NGitLab/PublicAPI/net472/PublicAPI.Unshipped.txt
index 8a26a2a5..371bc878 100644
--- a/NGitLab/PublicAPI/net472/PublicAPI.Unshipped.txt
+++ b/NGitLab/PublicAPI/net472/PublicAPI.Unshipped.txt
@@ -1565,6 +1565,8 @@ NGitLab.Models.CommitCherryPick.Sha.set -> void
NGitLab.Models.CommitCreate
NGitLab.Models.CommitCreate.Actions.get -> System.Collections.Generic.IList
NGitLab.Models.CommitCreate.Actions.set -> void
+NGitLab.Models.CommitCreate.AllowEmpty.get -> bool?
+NGitLab.Models.CommitCreate.AllowEmpty.set -> void
NGitLab.Models.CommitCreate.AuthorEmail.get -> string
NGitLab.Models.CommitCreate.AuthorEmail.set -> void
NGitLab.Models.CommitCreate.AuthorName.get -> string
diff --git a/NGitLab/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/NGitLab/PublicAPI/net8.0/PublicAPI.Unshipped.txt
index 8bfcb4d7..398a4b86 100644
--- a/NGitLab/PublicAPI/net8.0/PublicAPI.Unshipped.txt
+++ b/NGitLab/PublicAPI/net8.0/PublicAPI.Unshipped.txt
@@ -1564,6 +1564,8 @@ NGitLab.Models.CommitCherryPick.Sha.set -> void
NGitLab.Models.CommitCreate
NGitLab.Models.CommitCreate.Actions.get -> System.Collections.Generic.IList
NGitLab.Models.CommitCreate.Actions.set -> void
+NGitLab.Models.CommitCreate.AllowEmpty.get -> bool?
+NGitLab.Models.CommitCreate.AllowEmpty.set -> void
NGitLab.Models.CommitCreate.AuthorEmail.get -> string
NGitLab.Models.CommitCreate.AuthorEmail.set -> void
NGitLab.Models.CommitCreate.AuthorName.get -> string
diff --git a/NGitLab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/NGitLab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
index 8a26a2a5..371bc878 100644
--- a/NGitLab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
+++ b/NGitLab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
@@ -1565,6 +1565,8 @@ NGitLab.Models.CommitCherryPick.Sha.set -> void
NGitLab.Models.CommitCreate
NGitLab.Models.CommitCreate.Actions.get -> System.Collections.Generic.IList
NGitLab.Models.CommitCreate.Actions.set -> void
+NGitLab.Models.CommitCreate.AllowEmpty.get -> bool?
+NGitLab.Models.CommitCreate.AllowEmpty.set -> void
NGitLab.Models.CommitCreate.AuthorEmail.get -> string
NGitLab.Models.CommitCreate.AuthorEmail.set -> void
NGitLab.Models.CommitCreate.AuthorName.get -> string