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
12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,9 @@
[![Coverage](https://codecov.io/gh/boxboxjason/gitlab-sync/branch/main/graph/badge.svg)](https://codecov.io/gh/boxboxjason/gitlab-sync)
![Go Version](https://img.shields.io/github/go-mod/go-version/boxboxjason/gitlab-sync)
[![Latest Release](https://img.shields.io/github/v/release/boxboxjason/gitlab-sync)](https://github.com/boxboxjason/gitlab-sync/releases)
[![Release Workflow](https://github.com/boxboxjason/gitlab-sync/actions/workflows/release.yml/badge.svg)](https://github.com/boxboxjason/gitlab-sync/actions/workflows/release.yml)
![Last Commit](https://img.shields.io/github/last-commit/boxboxjason/gitlab-sync)
[![Tests](https://github.com/boxboxjason/gitlab-sync/actions/workflows/go.yml/badge.svg)](https://github.com/boxboxjason/gitlab-sync/actions/workflows/go.yml)
![Stars](https://img.shields.io/github/stars/boxboxjason/gitlab-sync)
![Contributors](https://img.shields.io/github/contributors/boxboxjason/gitlab-sync?style=social)
![Issues](https://img.shields.io/github/issues/boxboxjason/gitlab-sync)
![Pull Requests](https://img.shields.io/github/issues-pr/boxboxjason/gitlab-sync)

(golang) CLI tool to synchronize GitLab projects and groups between two GitLab instances.
It is designed to be used in a CI/CD pipeline to automate the process of keeping two GitLab instances in sync.
Expand Down Expand Up @@ -74,6 +70,8 @@ If mandatory arguments are not provided, the program will prompt for them.
| `--source-token` | `SOURCE_GITLAB_TOKEN` | No | Access token for the source GitLab instance |
| `--source-big` | `SOURCE_GITLAB_BIG` | No | Specify if the source GitLab instance is a big instance (default: false) |
| `--destination-url` | `DESTINATION_GITLAB_URL` | Yes | URL of the destination GitLab instance |
| `--destination-force-freemium` or `-f` | N/A | No | Force the destination GitLab to be treated as a non-premium instance (default: false) |
| `--destination-force-premium` or `-p` | N/A | No | Force the destination GitLab to be treated as a premium instance (default: false) |
| `--destination-token` | `DESTINATION_GITLAB_TOKEN` | Yes | Access token for the destination GitLab instance |
| `--destination-big` | `DESTINATION_GITLAB_BIG` | No | Specify if the destination GitLab instance is a big instance (default: false) |
| `--mirror-mapping` | `MIRROR_MAPPING` | Yes | Path to a JSON file containing the mirror mapping |
Expand Down Expand Up @@ -101,7 +99,7 @@ Allowed options are:
|--------|-------------|
| `destination_path` | The path to the project / group on the destination GitLab instance. |
| `ci_cd_catalog` | Whether to add the project to the CI/CD catalog. |
| `issues` | Whether to copy issues from the source project to the destination project. |
| `mirror_issues` | Whether to copy issues from the source project to the destination project. |
| `visibility` | The visibility level of the project on the destination GitLab instance. Can be `public`, `internal`, or `private`. |
| `mirror_trigger_builds` | Whether to trigger builds on the destination project when a push is made to the source project. |
| `mirror_releases` | Whether to mirror releases from the source project to the destination project. |
Expand All @@ -116,7 +114,7 @@ Also, the destination namespace must exist on the destination GitLab instance. I
"existingGroup1/project1" : {
"destination_path": "existingGroup64/project1",
"ci_cd_catalog": true,
"issues": false,
"mirror_issues": false,
"visibility": "public",
"mirror_trigger_builds": false,
"mirror_releases": false
Expand All @@ -126,7 +124,7 @@ Also, the destination namespace must exist on the destination GitLab instance. I
"existingGroup152" : {
"destination_path": "existingGroup64/existingGroup152",
"ci_cd_catalog": true,
"issues": false,
"mirror_issues": false,
"visibility": "public",
"mirror_trigger_builds": false,
"mirror_releases": false
Expand Down
25 changes: 0 additions & 25 deletions internal/mirroring/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"gitlab-sync/internal/utils"
"gitlab-sync/pkg/helpers"
"path/filepath"
"strings"
"sync"

"github.com/Masterminds/semver/v3"
Expand Down Expand Up @@ -62,30 +61,6 @@ func (g *GitlabInstance) getParentNamespaceID(projectOrGroupPath string) (int, e
return parentGroupID, err
}

// checkPathMatchesFilters checks if the resources matches the filters
// - either is in the projects map
// - or path starts with any of the groups in the groups map
//
// In the case of a match with a group, it returns the group path
func checkPathMatchesFilters(resourcePath string, projectFilters *map[string]struct{}, groupFilters *map[string]struct{}) (string, bool) {
zap.L().Debug("Checking if path matches filters", zap.String("path", resourcePath))
if projectFilters != nil {
if _, ok := (*projectFilters)[resourcePath]; ok {
zap.L().Debug("Resource path matches project filter", zap.String("project", resourcePath))
return "", true
}
}
if groupFilters != nil {
for groupPath := range *groupFilters {
if strings.HasPrefix(resourcePath, groupPath) {
zap.L().Debug("Resource path matches group filter", zap.String("resource", resourcePath), zap.String("group", groupPath))
return groupPath, true
}
}
}
return "", false
}

// IsVersionGreaterThanThreshold checks if the GitLab instance version is below the defined threshold.
// It retrieves the metadata from the GitLab instance and compares the version
// with the INSTANCE_SEMVER_THRESHOLD.
Expand Down
4 changes: 2 additions & 2 deletions internal/mirroring/get_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (g *GitlabInstance) storeGroup(group *gitlab.Group, parentGroupPath string,
mirrorMapping.AddGroup(group.FullPath, &utils.MirroringOptions{
DestinationPath: filepath.Join(groupCreationOptions.DestinationPath, relativePath),
CI_CD_Catalog: groupCreationOptions.CI_CD_Catalog,
Issues: groupCreationOptions.Issues,
MirrorIssues: groupCreationOptions.MirrorIssues,
MirrorTriggerBuilds: groupCreationOptions.MirrorTriggerBuilds,
Visibility: groupCreationOptions.Visibility,
MirrorReleases: groupCreationOptions.MirrorReleases,
Expand Down Expand Up @@ -121,7 +121,7 @@ func (g *GitlabInstance) processGroupsSmallInstance(allGroups []*gitlab.Group, g
go func(group *gitlab.Group) {
defer wg.Done()

groupPath, matches := checkPathMatchesFilters(group.FullPath, nil, groupFilters)
groupPath, matches := helpers.MatchPathAgainstFilters(group.FullPath, nil, groupFilters)
if matches {
g.storeGroup(group, groupPath, mirrorMapping)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/mirroring/get_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (g *GitlabInstance) storeProject(project *gitlab.Project, parentGroupPath s
mirrorMapping.AddProject(project.PathWithNamespace, &utils.MirroringOptions{
DestinationPath: filepath.Join(groupCreationOptions.DestinationPath, relativePath),
CI_CD_Catalog: groupCreationOptions.CI_CD_Catalog,
Issues: groupCreationOptions.Issues,
MirrorIssues: groupCreationOptions.MirrorIssues,
MirrorTriggerBuilds: groupCreationOptions.MirrorTriggerBuilds,
Visibility: groupCreationOptions.Visibility,
MirrorReleases: groupCreationOptions.MirrorReleases,
Expand Down Expand Up @@ -127,7 +127,7 @@ func (g *GitlabInstance) processProjectsSmallInstance(allProjects []*gitlab.Proj
go func(project *gitlab.Project) {
defer wg.Done()

group, matches := checkPathMatchesFilters(project.PathWithNamespace, projectFilters, groupFilters)
group, matches := helpers.MatchPathAgainstFilters(project.PathWithNamespace, projectFilters, groupFilters)
if matches {
g.storeProject(project, group, mirrorMapping)
}
Expand Down
3 changes: 2 additions & 1 deletion internal/mirroring/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package mirroring

import (
"gitlab-sync/internal/utils"
"gitlab-sync/pkg/helpers"
"net/http"
"testing"
)
Expand Down Expand Up @@ -61,7 +62,7 @@ func TestCheckPathMatchesFilters(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
// Call the function with the test case parameters
_, got := checkPathMatchesFilters(test.path, &test.projectFilters, &test.groupFilters)
_, got := helpers.MatchPathAgainstFilters(test.path, &test.projectFilters, &test.groupFilters)

// Check if the result matches the expected value
if got != test.expected {
Expand Down
139 changes: 139 additions & 0 deletions internal/mirroring/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,112 @@ var (
"links": []
}
}`}

issue_string = "issue"

TEST_ISSUE = &gitlab.Issue{
IID: 1,
Title: "Test Issue",
Description: "This is a test issue",
Labels: []string{"bug", "urgent"},
Confidential: false,
Weight: 3,
IssueType: &issue_string,
}

// TEST_ISSUE_STRING is the string representation of TEST_ISSUE.
TEST_ISSUE_STRING = `{
"project_id" : 1,
"milestone" : {
"due_date" : null,
"project_id" : 4,
"state" : "closed",
"description" : "Rerum est voluptatem provident consequuntur molestias similique ipsum dolor.",
"iid" : 3,
"id" : 11,
"title" : "v3.0",
"created_at" : "2016-01-04T15:31:39.788Z",
"updated_at" : "2016-01-04T15:31:39.788Z"
},
"author" : {
"state" : "active",
"web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root",
"id" : 1,
"name" : "Administrator"
},
"description" : "Omnis vero earum sunt corporis dolor et placeat.",
"state" : "closed",
"iid" : 1,
"assignees" : [{
"avatar_url" : null,
"web_url" : "https://gitlab.example.com/lennie",
"state" : "active",
"username" : "lennie",
"id" : 9,
"name" : "Dr. Luella Kovacek"
}],
"assignee" : {
"avatar_url" : null,
"web_url" : "https://gitlab.example.com/lennie",
"state" : "active",
"username" : "lennie",
"id" : 9,
"name" : "Dr. Luella Kovacek"
},
"type" : "ISSUE",
"labels" : ["foo", "bar"],
"upvotes": 4,
"downvotes": 0,
"merge_requests_count": 0,
"id" : 41,
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z",
"closed_at" : "2016-01-05T15:31:46.176Z",
"closed_by" : {
"state" : "active",
"web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root",
"id" : 1,
"name" : "Administrator"
},
"user_notes_count": 1,
"due_date": "2016-07-22",
"imported": false,
"imported_from": "none",
"web_url": "http://gitlab.example.com/my-group/my-project/issues/1",
"references": {
"short": "#1",
"relative": "#1",
"full": "my-group/my-project#1"
},
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
"human_time_estimate": null,
"human_total_time_spent": null
},
"has_tasks": true,
"task_status": "10 of 15 tasks completed",
"confidential": false,
"discussion_locked": false,
"issue_type": "issue",
"severity": "UNKNOWN",
"_links":{
"self":"http://gitlab.example.com/api/v4/projects/4/issues/41",
"notes":"http://gitlab.example.com/api/v4/projects/4/issues/41/notes",
"award_emoji":"http://gitlab.example.com/api/v4/projects/4/issues/41/award_emoji",
"project":"http://gitlab.example.com/api/v4/projects/4",
"closed_as_duplicate_of": "http://gitlab.example.com/api/v4/projects/1/issues/75"
},
"task_completion_status":{
"count":0,
"completed_count":0
}
}`
)

func setupEmptyTestServer(t *testing.T, role string, instanceSize string) (*http.ServeMux, *GitlabInstance) {
Expand Down Expand Up @@ -552,6 +658,39 @@ func setupTestProject(mux *http.ServeMux, project *gitlab.Project, stringRespons
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "{}")
})
// Setup the get project issues response from the project ID
mux.HandleFunc(fmt.Sprintf("/api/v4/projects/%d/issues", project.ID), func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
w.Header().Set(HEADER_CONTENT_TYPE, HEADER_ACCEPT)
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "[%s]", TEST_ISSUE_STRING)
case http.MethodPost:
w.Header().Set(HEADER_CONTENT_TYPE, HEADER_ACCEPT)
w.WriteHeader(http.StatusCreated)
fmt.Fprint(w, TEST_ISSUE_STRING)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
})
// Setup the get project issue response from the project ID and issue IID
mux.HandleFunc(fmt.Sprintf("/api/v4/projects/%d/issues/%d", project.ID, TEST_ISSUE.IID), func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
w.Header().Set(HEADER_CONTENT_TYPE, HEADER_ACCEPT)
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, TEST_ISSUE_STRING)
case http.MethodPut:
w.Header().Set(HEADER_CONTENT_TYPE, HEADER_ACCEPT)
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, TEST_ISSUE_STRING)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
})
// Setup the get project issue notes response from the project ID and issue IID
}

func TestReverseGroupMirrorMap(t *testing.T) {
Expand Down
Loading
Loading