Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
60da4b4
Initial commit for gl2gh. Copy and adapt bbs2gh for GitLab.
synthead Apr 7, 2026
ce5e253
Merge branch 'main' into add-gitlab-archive-adapter
synthead Apr 23, 2026
b9e52ac
Update GitlabApi to use GitLab API routes.
synthead Apr 29, 2026
1f689b7
Rename GitlabRepository with GitlabProject.
synthead Apr 29, 2026
9990aec
Don't pass credentials to GetRepositoryAndAttachmentsSize.
synthead Apr 29, 2026
1d3d489
Use correct "group" and "project" verbs in GitlabInspectorService.
synthead Apr 29, 2026
a8dce15
Add GetAsyncHttpResponseMessage.
synthead Apr 30, 2026
44d2fc4
Use repoPath in GitlabApi.
synthead Apr 30, 2026
85bb039
Omit simple=true in GitlabApi.
synthead Apr 30, 2026
e817605
Add GetMergeRequestCount to GitlabApi.
synthead Apr 30, 2026
33d8c21
Update ReposCsvGeneratorService to use GitLab.
synthead Apr 30, 2026
cf1bbd3
Use GroupsCsvGeneratorService for GitLab.
synthead Apr 30, 2026
93fca3d
Use groups in GitLab InventoryReportCommandHandler.
synthead Apr 30, 2026
6a6c46c
Use projects in GitLab InventoryReportCommandHandler.
synthead Apr 30, 2026
02f2113
Use ProjectsCsvGeneratorService for GitLab projects.
synthead Apr 30, 2026
8bcb543
Use Path in GitlabProject.
synthead Apr 30, 2026
89d45d2
Use groups and projects in GitLab InventoryReportCommand.
synthead Apr 30, 2026
5b876ad
Replace bbs with gitlab in InventoryReportCommand.
synthead Apr 30, 2026
252fd41
Use GitlabGroup in GitLab InventoryReportCommand.
synthead Apr 30, 2026
c97b47e
Use GitlabGroup in GitLab InventoryReportCommandArgs.
synthead Apr 30, 2026
50d4725
Use ProjectsCsvGeneratorService in GitLab Program.cs.
synthead Apr 30, 2026
399b919
Use groups in GitLab GenerateScriptCommandHandler.
synthead Apr 30, 2026
6453e84
Use GitlabGroup in GitLab GenerateScriptCommandArgs.
synthead Apr 30, 2026
becffbd
Use group paths in GitLab InventoryReportCommandHandler.
synthead Apr 30, 2026
29ba355
Use merge requests in GitLab ProjectsCsvGeneratorService.
synthead Apr 30, 2026
a5fef91
Use Path var in GitLabApi.
synthead Apr 30, 2026
f08020d
Call GetProjects from GitLab GenerateScriptCommandHandler.
synthead Apr 30, 2026
b8c9925
Use GetMergeRequestCount in GitLab ProjectsCsvGeneratorService.
synthead Apr 30, 2026
d4c8782
Use projectPath var in GitlabApi.
synthead Apr 30, 2026
bacdf1b
Use merge requests in GitLab GitlabInspectorService.
synthead Apr 30, 2026
48f7a68
Use merge requests in GitLab GroupsCsvGeneratorService.
synthead Apr 30, 2026
693d99a
Call GetProjectMergeRequestCount from GitLab ProjectsCsvGeneratorServ…
synthead Apr 30, 2026
890c4c1
Use groups and projects in GitLab MigrateRepoCommandHandler.
synthead Apr 30, 2026
17c8c8d
Consume "archived" value from project API for GitLab.
synthead Apr 30, 2026
fa6e445
Use group and project in GitLab MigrateRepoCommandArgs.
synthead Apr 30, 2026
a6a62f4
Use gitlab options in GitLab MigrateRepoCommandArgs.
synthead Apr 30, 2026
3a2c55a
Pass group and project to GetExport from GL MigrateRepoCommandHandler.
synthead Apr 30, 2026
6445ece
Return Path from GetGroups in GitlabInspectorService.
synthead May 11, 2026
6c7702c
Use GetProjectMergeRequestCount in GitlabInspectorService.
synthead May 11, 2026
ba99ac7
Remove username from GitLab auth.
synthead May 13, 2026
700b902
Use GitLab references in GitlabApiFactory.
synthead May 13, 2026
e5f0c72
Remove GITLAB_USERNAME env var from EnvironmentVariableProvider.
synthead May 13, 2026
5149a20
Use PAT args for GitLab.
synthead May 13, 2026
243d3af
Use IGitlabArchiveDownloader.cs for IGitlabArchiveDownloader.
synthead May 13, 2026
23e9fc5
Remove Samba and SSH options from GL GenerateScriptCommandHandler.
synthead May 13, 2026
3334f13
Update GL GenerateScriptCommandHandler to use GitLab options.
synthead May 13, 2026
72ac8ec
Remove Message from GotlabApi GetExport response.
synthead May 13, 2026
1fab75a
Update GL MigrateRepoCommand to use GitLab options.
synthead May 13, 2026
e2178a9
Use GitLab states in GL ExportState.
synthead May 13, 2026
662dd87
Return message from GitlabApi StartExport.
synthead May 13, 2026
3892154
Remove GitLab archive downloader.
synthead May 14, 2026
3e08782
Use GITLAB type for GitLab connector.
synthead May 14, 2026
cb7792a
Add octoshift_ll_gitlab_self_serve FF in headers.
synthead May 14, 2026
86ac89e
Add DownloadToFile to GitlabClient.
synthead May 14, 2026
a8b79ed
Add DownloadExportArchive to GitlabApi.
synthead May 14, 2026
5a3f518
Download GitLab archives with HttpDownloadService.
synthead May 14, 2026
b5a2337
Remove Samba and SSH args from MigrateRepoCommandArgs.
synthead May 14, 2026
4492c19
Remove Samba and SSH flow from GL GenerateScriptCommand.
synthead May 14, 2026
2cc70c6
Remove GitlabUsername from GL InventoryReportCommandArgs.
synthead May 14, 2026
c245bd2
Always include archived status in projects CSVs.
synthead May 14, 2026
7fe146f
Use GitLab terminology in GL InventoryReportCommand.
synthead May 14, 2026
80441cb
Remove Samba and shared home logic from GL MigrateRepoCommandHandler.
synthead May 14, 2026
f89ac2c
Merge branch 'main' into add-gitlab-archive-adapter
synthead May 18, 2026
dc467e4
delete stale tests and unused gitlabsettings
brianaj May 19, 2026
edf51a9
remove unused packagereferences in csproj
brianaj May 19, 2026
ebd01da
remove Kerberos
brianaj May 19, 2026
a37f4ac
clean up Kerberos from migrate repo
brianaj May 19, 2026
fd61692
clean-up bbs in inventoryreport
brianaj May 19, 2026
2419299
fix some existing tests
brianaj May 19, 2026
8c6a8c8
update generate script command
brianaj May 19, 2026
a5359a9
get migrate repo tests passing
brianaj May 19, 2026
fd6b9bb
fix inventory report command
brianaj May 20, 2026
2eaaf5e
log version and if CE or not
brianaj May 20, 2026
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
9 changes: 9 additions & 0 deletions src/Octoshift/Models/GitlabProject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Octoshift.Models;

public record GitlabProject
{
public string Id { get; init; }
public string Name { get; init; }
public string Path { get; init; }
public bool Archived { get; init; }
}
4 changes: 4 additions & 0 deletions src/Octoshift/Services/EnvironmentVariableProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class EnvironmentVariableProvider
private const string AWS_REGION = "AWS_REGION";
private const string BBS_USERNAME = "BBS_USERNAME";
private const string BBS_PASSWORD = "BBS_PASSWORD";
private const string GITLAB_PAT = "GITLAB_PAT";
private const string SMB_PASSWORD = "SMB_PASSWORD";
private const string GEI_SKIP_STATUS_CHECK = "GEI_SKIP_STATUS_CHECK";
private const string GEI_SKIP_VERSION_CHECK = "GEI_SKIP_VERSION_CHECK";
Expand Down Expand Up @@ -57,6 +58,9 @@ public virtual string BbsUsername(bool throwIfNotFound = true) =>
public virtual string BbsPassword(bool throwIfNotFound = true) =>
GetSecret(BBS_PASSWORD, throwIfNotFound);

public virtual string GitlabPat(bool throwIfNotFound = true) =>
GetSecret(GITLAB_PAT, throwIfNotFound);

public virtual string SmbPassword(bool throwIfNotFound = true) =>
GetSecret(SMB_PASSWORD, throwIfNotFound);

Expand Down
41 changes: 41 additions & 0 deletions src/Octoshift/Services/GithubApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,30 @@ public virtual async Task<string> CreateBbsMigrationSource(string orgId)
return (string)data["data"]["createMigrationSource"]["migrationSource"]["id"];
}

public virtual async Task<string> CreateGitlabMigrationSource(string orgId)
{
var url = $"{_apiUrl}/graphql";

var query = "mutation createMigrationSource($name: String!, $url: String!, $ownerId: ID!, $type: MigrationSourceType!)";
var gql = "createMigrationSource(input: {name: $name, url: $url, ownerId: $ownerId, type: $type}) { migrationSource { id, name, url, type } }";

var payload = new
{
query = $"{query} {{ {gql} }}",
variables = new
{
name = "GitLab Source",
url = "https://not-used",
ownerId = orgId,
type = "GITLAB"
},
operationName = "createMigrationSource"
};

var data = await _client.PostGraphQLAsync(url, payload);
return (string)data["data"]["createMigrationSource"]["migrationSource"]["id"];
}

public virtual async Task<string> CreateGhecMigrationSource(string orgId)
{
var url = $"{_apiUrl}/graphql";
Expand Down Expand Up @@ -534,6 +558,23 @@ public virtual async Task<string> StartBbsMigration(string migrationSourceId, st
);
}

public virtual async Task<string> StartGitlabMigration(string migrationSourceId, string gitlabRepoUrl, string orgId, string repo, string targetToken, string archiveUrl, string targetRepoVisibility = null)
{
return await StartMigration(
migrationSourceId,
gitlabRepoUrl, // source repository URL
orgId,
repo,
"not-used", // source access token
targetToken,
archiveUrl,
null, // GitLab archive contains both git and metadata — GitHub falls back to gitArchiveUrl
false, // skip releases
targetRepoVisibility,
false // lock source
);
}

public virtual async Task<(string State, string RepositoryName, int WarningsCount, string FailureReason, string MigrationLogUrl)> GetMigration(string migrationId)
{
var url = $"{_apiUrl}/graphql";
Expand Down
2 changes: 1 addition & 1 deletion src/Octoshift/Services/GithubClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public GithubClient(OctoLogger log, HttpClient httpClient, IVersionProvider vers
if (_httpClient != null)
{
_httpClient.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
_httpClient.DefaultRequestHeaders.Add("GraphQL-Features", "import_api,mannequin_claiming_emu,org_import_api");
_httpClient.DefaultRequestHeaders.Add("GraphQL-Features", "import_api,mannequin_claiming_emu,org_import_api,octoshift_ll_gitlab_self_serve");
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", personalAccessToken);
_httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("OctoshiftCLI", versionProvider?.GetCurrentVersion()));
if (versionProvider?.GetVersionComments() is { } comments)
Expand Down
175 changes: 175 additions & 0 deletions src/Octoshift/Services/GitlabApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using OctoshiftCLI.Extensions;

namespace OctoshiftCLI.Services;

public class GitlabApi
{
private readonly GitlabClient _client;
private readonly string _gitlabBaseUrl;
private readonly OctoLogger _log;

public GitlabApi(GitlabClient client, string gitlabServerUrl, OctoLogger log)
{
_client = client;
_gitlabBaseUrl = gitlabServerUrl?.TrimEnd('/');
_log = log;
}

public virtual async Task<(string Version, bool Enterprise)> GetServerVersion()
{
var url = $"{_gitlabBaseUrl}/api/v4/version";

var content = await _client.GetAsync(url);
var data = JObject.Parse(content);

return ((string)data["version"], (bool?)data["enterprise"] ?? false);
}

public virtual async Task LogServerVersion()
{
var (version, enterprise) = await GetServerVersion();
if (!string.IsNullOrWhiteSpace(version))
{
var edition = enterprise ? "Enterprise" : "Community";
_log?.LogInformation($"GitLab version: {version} ({edition} Edition)");
}
}

public virtual async Task<string> StartExport(string groupPath, string projectPath)
{
var encodedProjectPath = GetEncodedProjectPath(groupPath, projectPath);
var url = $"{_gitlabBaseUrl}/api/v4/projects/{encodedProjectPath}/export";

var exportResponse = await _client.PostAsync(url, new { });
var exportData = JObject.Parse(exportResponse);

return (string)exportData["message"];
}

public virtual async Task<(string ExportStatus, string DownloadUrl)> GetExport(string groupPath, string projectPath)
{
var encodedProjectPath = GetEncodedProjectPath(groupPath, projectPath);
var url = $"{_gitlabBaseUrl}/api/v4/projects/{encodedProjectPath}/export";

var exportResponse = await _client.GetAsync(url);
var exportData = JObject.Parse(exportResponse);

return (
(string)exportData["export_status"],
(string)exportData["_links"]?["api_url"]
);
}

public virtual async Task DownloadExportArchive(string groupPath, string projectPath, string file)
{
var encodedProjectPath = GetEncodedProjectPath(groupPath, projectPath);
var url = $"{_gitlabBaseUrl}/api/v4/projects/{encodedProjectPath}/export/download";

await _client.DownloadToFile(url, file);
}

public virtual async Task<IEnumerable<(long Id, string Path, string Name, bool Archived)>> GetProjects(string groupPath)
{
var encodedGroupPath = Uri.EscapeDataString(groupPath);
var url = $"{_gitlabBaseUrl}/api/v4/groups/{encodedGroupPath}/projects?per_page=100";

return await _client.GetAllAsync(url)
.Select(x => ((long)x["id"], (string)x["path"], (string)x["name"], (bool)x["archived"]))
.ToListAsync();
}

public virtual async Task<(long Id, string Path, string Name)> GetGroup(string groupPath)
{
var encodedGroupPath = groupPath.EscapeDataString();
var url = $"{_gitlabBaseUrl}/api/v4/groups/{encodedGroupPath}";

var groupResponse = await _client.GetAsync(url);
var groupData = JObject.Parse(groupResponse);

return (
(long)groupData["id"],
(string)groupData["full_path"],
(string)groupData["name"]
);
}

public virtual async Task<IEnumerable<(long Id, string Path, string Name)>> GetGroups()
{
var url = $"{_gitlabBaseUrl}/api/v4/groups?per_page=100";

return await _client.GetAllAsync(url)
.Select(x => ((long)x["id"], (string)x["full_path"], (string)x["name"]))
.ToListAsync();
}

public virtual async Task<bool> GetIsProjectArchived(string groupPath, string projectPath)
{
var encodedProjectPath = GetEncodedProjectPath(groupPath, projectPath);
var url = $"{_gitlabBaseUrl}/api/v4/projects/{encodedProjectPath}";

var projectResponse = await _client.GetAsync(url);
var projectData = JObject.Parse(projectResponse);

return (bool)projectData["archived"];
}

public virtual async Task<DateTimeOffset?> GetRepositoryLatestCommitDate(string groupPath, string projectPath)
{
var encodedProjectPath = GetEncodedProjectPath(groupPath, projectPath);
var url = $"{_gitlabBaseUrl}/api/v4/projects/{encodedProjectPath}/repository/commits?per_page=1";

// Empty projects (no Git repo yet) return 404 here; treat as no commits.
var commitsResponse = await _client.GetOrNullForNotFoundAsync(url);
if (commitsResponse is null)
{
return null;
}

var commitsData = JArray.Parse(commitsResponse);
var lastCommittedDate = (string)commitsData.First?["committed_date"];

if (string.IsNullOrWhiteSpace(lastCommittedDate))

Check warning on line 136 in src/Octoshift/Services/GitlabApi.cs

View workflow job for this annotation

GitHub Actions / build (macos-latest, csharp)

'if' statement can be simplified

Check warning on line 136 in src/Octoshift/Services/GitlabApi.cs

View workflow job for this annotation

GitHub Actions / build (macos-latest, actions)

'if' statement can be simplified

Check warning on line 136 in src/Octoshift/Services/GitlabApi.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, csharp)

'if' statement can be simplified

Check warning on line 136 in src/Octoshift/Services/GitlabApi.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, actions)

'if' statement can be simplified

Check warning on line 136 in src/Octoshift/Services/GitlabApi.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest, actions)

'if' statement can be simplified

Check warning on line 136 in src/Octoshift/Services/GitlabApi.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest, csharp)

'if' statement can be simplified
{
return null;
}

return DateTimeOffset.Parse(lastCommittedDate);
}

public virtual async Task<(long RepositorySize, long AttachmentsSize)> GetRepositoryAndAttachmentsSize(string groupPath, string projectPath)
{
var encodedProjectPath = GetEncodedProjectPath(groupPath, projectPath);
var url = $"{_gitlabBaseUrl}/api/v4/projects/{encodedProjectPath}?statistics=true";

var projectResponse = await _client.GetAsync(url);
var projectData = JObject.Parse(projectResponse);
var projectStatistics = (JObject)projectData["statistics"];

var repositorySize = (long)projectStatistics["repository_size"];
var attachmentsSize = (long)projectStatistics["uploads_size"];

return (repositorySize, attachmentsSize);
}

public virtual async Task<int> GetMergeRequestCount(string groupPath, string projectPath)
{
var encodedProjectPath = GetEncodedProjectPath(groupPath, projectPath);
var url = $"{_gitlabBaseUrl}/api/v4/projects/{encodedProjectPath}/merge_requests?state=all&per_page=1&page=1";

var mrResponse = await _client.GetAsyncHttpResponseMessage(url);
var mrTotal = mrResponse.Headers.GetValues("X-Total").Single();

return int.Parse(mrTotal);
}

private static string GetEncodedProjectPath(string groupPath, string projectPath)
{
var pathWithNamespace = $"{groupPath}/{projectPath}";
return pathWithNamespace.EscapeDataString();
}
}
Loading
Loading