Repository: argoproj-labs/gitops-promoter
Pull request #205: feat: add gitlab support
Author: login: robinlieb
Created: 2025-03-02T13:35:12Z | Merged: 2025-03-27T19:33:52Z

Implements #141


Diff:
diff --git a/api/v1alpha1/common_types.go b/api/v1alpha1/common_types.go
index 093b6a18e..5ec938c31 100644
--- a/api/v1alpha1/common_types.go
+++ b/api/v1alpha1/common_types.go
@@ -4,6 +4,10 @@ type GitHub struct {
 	Domain string `json:"domain,omitempty"`
 }
 
+type GitLab struct {
+	Domain string `json:"domain,omitempty"`
+}
+
 type Fake struct {
 	Domain string `json:"domain,omitempty"`
 }
@@ -12,3 +16,30 @@ type ObjectReference struct {
 	// +kubebuilder:validation:Required
 	Name string `json:"name"`
 }
+
+type GitHubRepo struct {
+	// +kubebuilder:validation:Required
+	Owner string `json:"owner"`
+	// +kubebuilder:validation:Required
+	Name string `json:"name"`
+}
+
+type GitLabRepo struct {
+	// User, group or group with subgroup (e.g. group/subgroup).
+	// +kubebuilder:validation:Required
+	// +kubebuilder:validation:Pattern=^[a-zA-Z0-9_\-\/.]+$
+	Namespace string `json:"namespace"`
+	// Project slug of the repository.
+	// +kubebuilder:validation:Required
+	// +kubebuilder:validation:Pattern=^[a-zA-Z0-9_\-\/.]+$
+	Name string `json:"name"`
+	// +kubebuilder:validation:Required
+	ProjectID int `json:"projectId"`
+}
+
+type FakeRepo struct {
+	// +kubebuilder:validation:Required
+	Owner string `json:"owner"`
+	// +kubebuilder:validation:Required
+	Name string `json:"name"`
+}
diff --git a/api/v1alpha1/gitrepository_types.go b/api/v1alpha1/gitrepository_types.go
index 03bfc61b1..75d56e2bb 100644
--- a/api/v1alpha1/gitrepository_types.go
+++ b/api/v1alpha1/gitrepository_types.go
@@ -25,10 +25,9 @@ import (
 
 // GitRepositorySpec defines the desired state of GitRepository
 type GitRepositorySpec struct {
-	// +kubebuilder:validation:Required
-	Owner string `json:"owner"`
-	// +kubebuilder:validation:Required
-	Name string `json:"name"`
+	GitHub *GitHubRepo `json:"github,omitempty"`
+	GitLab *GitLabRepo `json:"gitlab,omitempty"`
+	Fake   *FakeRepo   `json:"fake,omitempty"`
 	// +kubebuilder:validation:Required
 	ScmProviderRef ObjectReference `json:"scmProviderRef"`
 }
diff --git a/api/v1alpha1/scmprovider_types.go b/api/v1alpha1/scmprovider_types.go
index 85cbff23e..7cad5cc52 100644
--- a/api/v1alpha1/scmprovider_types.go
+++ b/api/v1alpha1/scmprovider_types.go
@@ -35,6 +35,9 @@ type ScmProviderSpec struct {
 	// GitHub required configuration for GitHub as the SCM provider
 	GitHub *GitHub `json:"github,omitempty"`
 
+	// GitLab required configuration for GitLab as the SCM provider
+	GitLab *GitLab `json:"gitlab,omitempty"`
+
 	// Fake required configuration for Fake as the SCM provider
 	Fake *Fake `json:"fake,omitempty"`
 }
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index e56b6842e..a036906d7 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -481,6 +481,21 @@ func (in *Fake) DeepCopy() *Fake {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FakeRepo) DeepCopyInto(out *FakeRepo) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FakeRepo.
+func (in *FakeRepo) DeepCopy() *FakeRepo {
+	if in == nil {
+		return nil
+	}
+	out := new(FakeRepo)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *GitHub) DeepCopyInto(out *GitHub) {
 	*out = *in
@@ -496,12 +511,57 @@ func (in *GitHub) DeepCopy() *GitHub {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GitHubRepo) DeepCopyInto(out *GitHubRepo) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubRepo.
+func (in *GitHubRepo) DeepCopy() *GitHubRepo {
+	if in == nil {
+		return nil
+	}
+	out := new(GitHubRepo)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GitLab) DeepCopyInto(out *GitLab) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitLab.
+func (in *GitLab) DeepCopy() *GitLab {
+	if in == nil {
+		return nil
+	}
+	out := new(GitLab)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GitLabRepo) DeepCopyInto(out *GitLabRepo) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitLabRepo.
+func (in *GitLabRepo) DeepCopy() *GitLabRepo {
+	if in == nil {
+		return nil
+	}
+	out := new(GitLabRepo)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *GitRepository) DeepCopyInto(out *GitRepository) {
 	*out = *in
 	out.TypeMeta = in.TypeMeta
 	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
-	out.Spec = in.Spec
+	in.Spec.DeepCopyInto(&out.Spec)
 	out.Status = in.Status
 }
 
@@ -558,6 +618,21 @@ func (in *GitRepositoryList) DeepCopyObject() runtime.Object {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *GitRepositorySpec) DeepCopyInto(out *GitRepositorySpec) {
 	*out = *in
+	if in.GitHub != nil {
+		in, out := &in.GitHub, &out.GitHub
+		*out = new(GitHubRepo)
+		**out = **in
+	}
+	if in.GitLab != nil {
+		in, out := &in.GitLab, &out.GitLab
+		*out = new(GitLabRepo)
+		**out = **in
+	}
+	if in.Fake != nil {
+		in, out := &in.Fake, &out.Fake
+		*out = new(FakeRepo)
+		**out = **in
+	}
 	out.ScmProviderRef = in.ScmProviderRef
 }
 
@@ -1137,6 +1212,11 @@ func (in *ScmProviderSpec) DeepCopyInto(out *ScmProviderSpec) {
 		*out = new(GitHub)
 		**out = **in
 	}
+	if in.GitLab != nil {
+		in, out := &in.GitLab, &out.GitLab
+		*out = new(GitLab)
+		**out = **in
+	}
 	if in.Fake != nil {
 		in, out := &in.Fake, &out.Fake
 		*out = new(Fake)
diff --git a/config/crd/bases/promoter.argoproj.io_gitrepositories.yaml b/config/crd/bases/promoter.argoproj.io_gitrepositories.yaml
index 80955a89e..363770a7f 100644
--- a/config/crd/bases/promoter.argoproj.io_gitrepositories.yaml
+++ b/config/crd/bases/promoter.argoproj.io_gitrepositories.yaml
@@ -39,10 +39,43 @@ spec:
           spec:
             description: GitRepositorySpec defines the desired state of GitRepository
             properties:
-              name:
-                type: string
-              owner:
-                type: string
+              fake:
+                properties:
+                  name:
+                    type: string
+                  owner:
+                    type: string
+                required:
+                - name
+                - owner
+                type: object
+              github:
+                properties:
+                  name:
+                    type: string
+                  owner:
+                    type: string
+                required:
+                - name
+                - owner
+                type: object
+              gitlab:
+                properties:
+                  name:
+                    description: Project slug of the repository.
+                    pattern: ^[a-zA-Z0-9_\-\/.]+$
+                    type: string
+                  namespace:
+                    description: User, group or group with subgroup (e.g. group/subgroup).
+                    pattern: ^[a-zA-Z0-9_\-\/.]+$
+                    type: string
+                  projectId:
+                    type: integer
+                required:
+                - name
+                - namespace
+                - projectId
+                type: object
               scmProviderRef:
                 properties:
                   name:
@@ -51,8 +84,6 @@ spec:
                 - name
                 type: object
             required:
-            - name
-            - owner
             - scmProviderRef
             type: object
           status:
diff --git a/config/crd/bases/promoter.argoproj.io_scmproviders.yaml b/config/crd/bases/promoter.argoproj.io_scmproviders.yaml
index 869d70f6d..4902c6cc9 100644
--- a/config/crd/bases/promoter.argoproj.io_scmproviders.yaml
+++ b/config/crd/bases/promoter.argoproj.io_scmproviders.yaml
@@ -51,6 +51,12 @@ spec:
                   domain:
                     type: string
                 type: object
+              gitlab:
+                description: GitLab required configuration for GitLab as the SCM provider
+                properties:
+                  domain:
+                    type: string
+                type: object
               secretRef:
                 description: SecretRef contains the credentials required to auth to
                   a specific provider
diff --git a/dist/install.yaml b/dist/install.yaml
index d1f2aa694..1ad15146b 100644
--- a/dist/install.yaml
+++ b/dist/install.yaml
@@ -482,10 +482,43 @@ spec:
           spec:
             description: GitRepositorySpec defines the desired state of GitRepository
             properties:
-              name:
-                type: string
-              owner:
-                type: string
+              fake:
+                properties:
+                  name:
+                    type: string
+                  owner:
+                    type: string
+                required:
+                - name
+                - owner
+                type: object
+              github:
+                properties:
+                  name:
+                    type: string
+                  owner:
+                    type: string
+                required:
+                - name
+                - owner
+                type: object
+              gitlab:
+                properties:
+                  name:
+                    description: Project slug of the repository.
+                    pattern: ^[a-zA-Z0-9_\-\/.]+$
+                    type: string
+                  namespace:
+                    description: User, group or group with subgroup (e.g. group/subgroup).
+                    pattern: ^[a-zA-Z0-9_\-\/.]+$
+                    type: string
+                  projectId:
+                    type: integer
+                required:
+                - name
+                - namespace
+                - projectId
+                type: object
               scmProviderRef:
                 properties:
                   name:
@@ -494,8 +527,6 @@ spec:
                 - name
                 type: object
             required:
-            - name
-            - owner
             - scmProviderRef
             type: object
           status:
@@ -1047,6 +1078,12 @@ spec:
                   domain:
                     type: string
                 type: object
+              gitlab:
+                description: GitLab required configuration for GitLab as the SCM provider
+                properties:
+                  domain:
+                    type: string
+                type: object
               secretRef:
                 description: SecretRef contains the credentials required to auth to
                   a specific provider
diff --git a/docs/getting-started.md b/docs/getting-started.md
index e666b537f..2feb53f44 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -1,7 +1,7 @@
 # Getting Started
 
 This guide will help you get started installing and setting up the GitOps Promoter. We currently only support
-GitHub and GitHub Enterprise as the SCM providers. We would welcome any contributions to add support for other
+GitHub, GitHub Enterprise and GitLab as the SCM providers. We would welcome any contributions to add support for other
 providers.
 
 ## Requirements
@@ -86,8 +86,9 @@ kind: GitRepository
 metadata:
   name: <git-repository-ref-name>
 spec:
-  name: <repo-name>
-  owner: <github-org-username>
+  github:
+    name: <repo-name>
+    owner: <github-org-username>
   scmProviderRef:
     name: <your-scmprovider-name> # The secret that contains the GitHub App configuration
 ```
@@ -97,6 +98,46 @@ spec:
     The GitRepository and ScmProvider also need to be installed to the same namespace that you plan on creating PromotionStrategy 
     resources in, and it also needs to be in the same namespace of the secret it references.
 
+## GitLab Configuration
+
+To configure the GitOps Promoter with GitLab, you will need to create a GitLab Access Token under the "Developer" role with `api` and `write_repository` scopes and configure the necessary resources to allow the promoter to interact with your repository. This Access Token should be used in a secret as follows:
+
+```yaml
+apiVersion: v1
+kind: Secret
+metadata:
+  name: <your-secret-name>
+type: Opaque
+stringData:
+  token: <your-access-token>
+```
+
+We also need a GitRepository and ScmProvider, which is are custom resources that represents a git repository and a provider. 
+Here is an example of both resources:
+
+```yaml
+apiVersion: promoter.argoproj.io/v1alpha1
+kind: ScmProvider
+metadata:
+  name: <your-scmprovider-name>
+spec:
+  secretRef:
+    name: <your-secret-name>
+  gitlab: {}
+---
+apiVersion: promoter.argoproj.io/v1alpha1
+kind: GitRepository
+metadata:
+  name: <git-repository-ref-name>
+spec:
+  gitlab:
+    name: <repo-name>
+    namespace: <user-or-group-with-subgroups>
+    projectId: <project-id>
+  scmProviderRef:
+    name: <your-scmprovider-name> # The secret that contains the GitLab Access Token
+```
+
 ## Promotion Strategy
 
 The PromotionStrategy resource is the main resource that you will use to configure the promotion of your application to different environments.
diff --git a/go.mod b/go.mod
index a1c8752b5..70f64ab32 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ toolchain go1.23.4
 require (
 	github.com/bradleyfalzon/ghinstallation/v2 v2.14.0
 	github.com/cespare/xxhash/v2 v2.3.0
+	github.com/go-logr/logr v1.4.2
 	github.com/go-task/slim-sprig/v3 v3.0.0
 	github.com/google/go-github/v61 v61.0.0
 	github.com/onsi/ginkgo/v2 v2.23.3
@@ -16,6 +17,7 @@ require (
 	github.com/spf13/pflag v1.0.6
 	github.com/stretchr/testify v1.10.0
 	github.com/tidwall/gjson v1.18.0
+	gitlab.com/gitlab-org/api/client-go v0.123.0
 	go.uber.org/zap v1.27.0
 	gopkg.in/yaml.v3 v3.0.1
 	k8s.io/api v0.32.3
@@ -32,7 +34,6 @@ require (
 	github.com/evanphx/json-patch/v5 v5.9.11 // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
-	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/zapr v1.3.0 // indirect
 	github.com/go-openapi/jsonpointer v0.21.0 // indirect
 	github.com/go-openapi/jsonreference v0.20.2 // indirect
@@ -49,6 +50,8 @@ require (
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
 	github.com/google/uuid v1.6.0 // indirect
+	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
+	github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
@@ -68,12 +71,12 @@ require (
 	go.uber.org/multierr v1.11.0 // indirect
 	golang.org/x/crypto v0.36.0 // indirect
 	golang.org/x/net v0.37.0 // indirect
-	golang.org/x/oauth2 v0.23.0 // indirect
+	golang.org/x/oauth2 v0.25.0 // indirect
 	golang.org/x/sync v0.12.0 // indirect
 	golang.org/x/sys v0.31.0 // indirect
 	golang.org/x/term v0.30.0 // indirect
 	golang.org/x/text v0.23.0 // indirect
-	golang.org/x/time v0.7.0 // indirect
+	golang.org/x/time v0.9.0 // indirect
 	golang.org/x/tools v0.30.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
 	google.golang.org/protobuf v1.36.5 // indirect
diff --git a/go.sum b/go.sum
index 1e9c27fab..58686f7b6 100644
--- a/go.sum
+++ b/go.sum
@@ -15,6 +15,8 @@ github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8
 github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
 github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
 github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
+github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
+github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
 github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
@@ -62,6 +64,12 @@ github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/Z
 github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
+github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
+github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
+github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
+github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -77,6 +85,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -132,6 +144,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+gitlab.com/gitlab-org/api/client-go v0.123.0 h1:W3LZ5QNyiSCJA0Zchkwz8nQIUzOuDoSWMZtRDT5DjPI=
+gitlab.com/gitlab-org/api/client-go v0.123.0/go.mod h1:Jh0qjLILEdbO6z/OY94RD+3NDQRUKiuFSFYozN6cpKM=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -153,8 +167,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
 golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
-golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
-golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
+golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -173,8 +187,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
 golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
-golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
-golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
+golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
diff --git a/internal/controller/argocdcommitstatus_controller.go b/internal/controller/argocdcommitstatus_controller.go
index eb00f8fd3..9bee607b1 100644
--- a/internal/controller/argocdcommitstatus_controller.go
+++ b/internal/controller/argocdcommitstatus_controller.go
@@ -30,6 +30,7 @@ import (
 	"github.com/argoproj-labs/gitops-promoter/internal/scms"
 	"github.com/argoproj-labs/gitops-promoter/internal/scms/fake"
 	"github.com/argoproj-labs/gitops-promoter/internal/scms/github"
+	"github.com/argoproj-labs/gitops-promoter/internal/scms/gitlab"
 
 	"github.com/argoproj-labs/gitops-promoter/internal/types/argocd"
 	"github.com/argoproj-labs/gitops-promoter/internal/utils"
@@ -394,6 +395,13 @@ func (r *ArgoCDCommitStatusReconciler) getGitAuthProvider(ctx context.Context, a
 	case scmProvider.Spec.GitHub != nil:
 		logger.V(4).Info("Creating GitHub git authentication provider")
 		return github.NewGithubGitAuthenticationProvider(scmProvider, secret), ps.Spec.RepositoryReference, nil
+	case scmProvider.Spec.GitLab != nil:
+		logger.V(4).Info("Creating GitLab git authentication provider")
+		gitlabClient, err := gitlab.NewGitlabGitAuthenticationProvider(scmProvider, secret)
+		if err != nil {
+			return nil, ps.Spec.RepositoryReference, fmt.Errorf("failed to create GitLab client: %w", err)
+		}
+		return gitlabClient, ps.Spec.RepositoryReference, nil
 	default:
 		return nil, ps.Spec.RepositoryReference, fmt.Errorf("no supported git authentication provider found")
 	}
diff --git a/internal/controller/changetransferpolicy_controller.go b/internal/controller/changetransferpolicy_controller.go
index 2c1ee9b29..27a1bd057 100644
--- a/internal/controller/changetransferpolicy_controller.go
+++ b/internal/controller/changetransferpolicy_controller.go
@@ -27,6 +27,7 @@ import (
 	"github.com/argoproj-labs/gitops-promoter/internal/scms"
 	"github.com/argoproj-labs/gitops-promoter/internal/scms/fake"
 	"github.com/argoproj-labs/gitops-promoter/internal/scms/github"
+	"github.com/argoproj-labs/gitops-promoter/internal/scms/gitlab"
 	"github.com/argoproj-labs/gitops-promoter/internal/settings"
 	"github.com/argoproj-labs/gitops-promoter/internal/utils"
 	v1 "k8s.io/api/core/v1"
@@ -177,6 +178,13 @@ func (r *ChangeTransferPolicyReconciler) getGitAuthProvider(ctx context.Context,
 	case scmProvider.Spec.GitHub != nil:
 		logger.V(4).Info("Creating GitHub git authentication provider")
 		return github.NewGithubGitAuthenticationProvider(scmProvider, secret), nil
+	case scmProvider.Spec.GitLab != nil:
+		logger.V(4).Info("Creating GitLab git authentication provider")
+		provider, err := gitlab.NewGitlabGitAuthenticationProvider(scmProvider, secret)
+		if err != nil {
+			return nil, fmt.Errorf("failed to create GitLab Auth Provider: %w", err)
+		}
+		return provider, nil
 	default:
 		return nil, fmt.Errorf("no supported git authentication provider found")
 	}
@@ -342,7 +350,18 @@ func (r *ChangeTransferPolicyReconciler) creatOrUpdatePullRequest(ctx context.Co
 	if err != nil {
 		return fmt.Errorf("failed to get GitRepository %q: %w", ctp.Spec.RepositoryReference.Name, err)
 	}
-	prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctp.Spec.ProposedBranch, ctp.Spec.ActiveBranch))
+
+	var prName string
+	switch {
+	case gitRepo.Spec.GitHub != nil:
+		prName = utils.GetPullRequestName(ctx, gitRepo.Spec.GitHub.Owner, gitRepo.Spec.GitHub.Name, ctp.Spec.ProposedBranch, ctp.Spec.ActiveBranch)
+	case gitRepo.Spec.GitLab != nil:
+		prName = utils.GetPullRequestName(ctx, gitRepo.Spec.GitLab.Namespace, gitRepo.Spec.GitLab.Name, ctp.Spec.ProposedBranch, ctp.Spec.ActiveBranch)
+	case gitRepo.Spec.Fake != nil:
+		prName = utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctp.Spec.ProposedBranch, ctp.Spec.ActiveBranch)
+	}
+
+	prName = utils.KubeSafeUniqueName(ctx, prName)
 
 	promotionConfig, err := r.SettingsMgr.GetPromotionConfiguration(ctx)
 	if err != nil {
diff --git a/internal/controller/changetransferpolicy_controller_test.go b/internal/controller/changetransferpolicy_controller_test.go
index 515d48933..f5d1dabf1 100644
--- a/internal/controller/changetransferpolicy_controller_test.go
+++ b/internal/controller/changetransferpolicy_controller_test.go
@@ -64,7 +64,7 @@ var _ = Describe("ChangeTransferPolicy Controller", func() {
 			Expect(err).NotTo(HaveOccurred())
 
 			By("Adding a pending commit")
-			fullSha, shortSha := makeChangeAndHydrateRepo(gitPath, gitRepo.Spec.Owner, gitRepo.Spec.Name)
+			fullSha, shortSha := makeChangeAndHydrateRepo(gitPath, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name)
 
 			By("Reconciling the created resource")
 
@@ -78,7 +78,7 @@ var _ = Describe("ChangeTransferPolicy Controller", func() {
 			}, EventuallyTimeout).Should(Succeed())
 
 			var pr promoterv1alpha1.PullRequest
-			prName := utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, changeTransferPolicy.Spec.ProposedBranch, changeTransferPolicy.Spec.ActiveBranch)
+			prName := utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, changeTransferPolicy.Spec.ProposedBranch, changeTransferPolicy.Spec.ActiveBranch)
 			Eventually(func(g Gomega) {
 				typeNamespacedNamePR := types.NamespacedName{
 					Name:      utils.KubeSafeUniqueName(ctx, prName),
@@ -92,7 +92,7 @@ var _ = Describe("ChangeTransferPolicy Controller", func() {
 			}, EventuallyTimeout).Should(Succeed())
 
 			By("Adding another pending commit")
-			_, shortSha = makeChangeAndHydrateRepo(gitPath, gitRepo.Spec.Owner, gitRepo.Spec.Name)
+			_, shortSha = makeChangeAndHydrateRepo(gitPath, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name)
 
 			simulateWebhook(ctx, k8sClient, changeTransferPolicy)
 			Eventually(func(g Gomega) {
@@ -159,7 +159,7 @@ var _ = Describe("ChangeTransferPolicy Controller", func() {
 			Expect(err).NotTo(HaveOccurred())
 
 			By("Adding a pending commit")
-			makeChangeAndHydrateRepo(gitPath, gitRepo.Spec.Owner, gitRepo.Spec.Name)
+			makeChangeAndHydrateRepo(gitPath, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name)
 
 			Eventually(func(g Gomega) {
 				err := k8sClient.Get(ctx, typeNamespacedName, commitStatus)
@@ -189,7 +189,7 @@ var _ = Describe("ChangeTransferPolicy Controller", func() {
 			}, EventuallyTimeout).Should(Succeed())
 
 			var pr promoterv1alpha1.PullRequest
-			prName := utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, changeTransferPolicy.Spec.ProposedBranch, changeTransferPolicy.Spec.ActiveBranch)
+			prName := utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, changeTransferPolicy.Spec.ProposedBranch, changeTransferPolicy.Spec.ActiveBranch)
 			Eventually(func(g Gomega) {
 				err = k8sClient.Get(ctx, typeNamespacedName, changeTransferPolicy)
 				Expect(err).To(Succeed())
@@ -234,7 +234,7 @@ var _ = Describe("ChangeTransferPolicy Controller", func() {
 			Expect(err).NotTo(HaveOccurred())
 
 			By("Adding a pending commit")
-			fullSha, shortSha := makeChangeAndHydrateRepo(gitPath, gitRepo.Spec.Owner, gitRepo.Spec.Name)
+			fullSha, shortSha := makeChangeAndHydrateRepo(gitPath, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name)
 
 			simulateWebhook(ctx, k8sClient, changeTransferPolicy)
 			Eventually(func(g Gomega) {
@@ -246,7 +246,7 @@ var _ = Describe("ChangeTransferPolicy Controller", func() {
 			}, EventuallyTimeout).Should(Succeed())
 
 			var pr promoterv1alpha1.PullRequest
-			prName := utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, changeTransferPolicy.Spec.ProposedBranch, changeTransferPolicy.Spec.ActiveBranch)
+			prName := utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, changeTransferPolicy.Spec.ProposedBranch, changeTransferPolicy.Spec.ActiveBranch)
 			Eventually(func(g Gomega) {
 				typeNamespacedNamePR := types.NamespacedName{
 					Name:      utils.KubeSafeUniqueName(ctx, prName),
@@ -316,8 +316,10 @@ func changeTransferPolicyResources(ctx context.Context, name, namespace string)
 			Namespace: namespace,
 		},
 		Spec: promoterv1alpha1.GitRepositorySpec{
-			Owner: name,
-			Name:  name,
+			Fake: &promoterv1alpha1.FakeRepo{
+				Owner: name,
+				Name:  name,
+			},
 			ScmProviderRef: promoterv1alpha1.ObjectReference{
 				Name: name,
 			},
diff --git a/internal/controller/commitstatus_controller.go b/internal/controller/commitstatus_controller.go
index 9b16bf261..8fa395803 100644
--- a/internal/controller/commitstatus_controller.go
+++ b/internal/controller/commitstatus_controller.go
@@ -33,6 +33,7 @@ import (
 
 	"github.com/argoproj-labs/gitops-promoter/internal/scms"
 	"github.com/argoproj-labs/gitops-promoter/internal/scms/github"
+	"github.com/argoproj-labs/gitops-promoter/internal/scms/gitlab"
 	"github.com/argoproj-labs/gitops-promoter/internal/utils"
 	"k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -169,6 +170,13 @@ func (r *CommitStatusReconciler) getCommitStatusProvider(ctx context.Context, co
 			return nil, fmt.Errorf("failed to get GitHub provider for domain %q with secret %q: %w", scmProvider.Spec.GitHub.Domain, secret.Name, err)
 		}
 		return p, nil
+	case scmProvider.Spec.GitLab != nil:
+		var p *gitlab.CommitStatus
+		p, err = gitlab.NewGitlabCommitStatusProvider(r.Client, *secret, scmProvider.Spec.GitLab.Domain)
+		if err != nil {
+			return nil, fmt.Errorf("failed to get GitLab provider for domain %q with secret %q: %w", scmProvider.Spec.GitLab.Domain, secret.Name, err)
+		}
+		return p, nil
 	case scmProvider.Spec.Fake != nil:
 		//nolint: wrapcheck
 		return fake.NewFakeCommitStatusProvider(*secret)
diff --git a/internal/controller/commitstatus_controller_test.go b/internal/controller/commitstatus_controller_test.go
index 9c905bc52..9b24eb8f7 100644
--- a/internal/controller/commitstatus_controller_test.go
+++ b/internal/controller/commitstatus_controller_test.go
@@ -76,8 +76,10 @@ var _ = Describe("CommitStatus Controller", func() {
 					Namespace: typeNamespacedName.Namespace,
 				},
 				Spec: promoterv1alpha1.GitRepositorySpec{
-					Owner: typeNamespacedName.Name,
-					Name:  typeNamespacedName.Name,
+					Fake: &promoterv1alpha1.FakeRepo{
+						Owner: typeNamespacedName.Name,
+						Name:  typeNamespacedName.Name,
+					},
 					ScmProviderRef: promoterv1alpha1.ObjectReference{
 						Name: typeNamespacedName.Name,
 					},
diff --git a/internal/controller/promotionstrategy_controller_test.go b/internal/controller/promotionstrategy_controller_test.go
index 56c191c9c..1c32ea3c5 100644
--- a/internal/controller/promotionstrategy_controller_test.go
+++ b/internal/controller/promotionstrategy_controller_test.go
@@ -88,7 +88,7 @@ var _ = Describe("PromotionStrategy Controller", func() {
 				}, &ctpProd)
 				g.Expect(err).To(Succeed())
 
-				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpDev.Spec.ProposedBranch, ctpDev.Spec.ActiveBranch))
+				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpDev.Spec.ProposedBranch, ctpDev.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -96,7 +96,7 @@ var _ = Describe("PromotionStrategy Controller", func() {
 				g.Expect(err).To(HaveOccurred())
 				g.Expect(errors.IsNotFound(err)).To(BeTrue())
 
-				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpStaging.Spec.ProposedBranch, ctpStaging.Spec.ActiveBranch))
+				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpStaging.Spec.ProposedBranch, ctpStaging.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -104,7 +104,7 @@ var _ = Describe("PromotionStrategy Controller", func() {
 				g.Expect(err).To(HaveOccurred())
 				g.Expect(errors.IsNotFound(err)).To(BeTrue())
 
-				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
+				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -178,7 +178,7 @@ var _ = Describe("PromotionStrategy Controller", func() {
 			By("Checking that the pull request for the development environment is created")
 			Eventually(func(g Gomega) {
 				// Dev PR should exist
-				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpDev.Spec.ProposedBranch, ctpDev.Spec.ActiveBranch))
+				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpDev.Spec.ProposedBranch, ctpDev.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -188,7 +188,7 @@ var _ = Describe("PromotionStrategy Controller", func() {
 
 			Eventually(func(g Gomega) {
 				// Staging PR should exist
-				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpStaging.Spec.ProposedBranch, ctpStaging.Spec.ActiveBranch))
+				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpStaging.Spec.ProposedBranch, ctpStaging.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -198,7 +198,7 @@ var _ = Describe("PromotionStrategy Controller", func() {
 
 			Eventually(func(g Gomega) {
 				// Production PR should exist
-				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
+				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -209,7 +209,7 @@ var _ = Describe("PromotionStrategy Controller", func() {
 			By("Checking that the pull request for the development, staging, and production environments are closed")
 			Eventually(func(g Gomega) {
 				// The PRs should eventually close because of no commit status checks configured
-				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpDev.Spec.ProposedBranch, ctpDev.Spec.ActiveBranch))
+				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpDev.Spec.ProposedBranch, ctpDev.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -217,7 +217,7 @@ var _ = Describe("PromotionStrategy Controller", func() {
 				g.Expect(err).To(HaveOccurred())
 				g.Expect(errors.IsNotFound(err)).To(BeTrue())
 
-				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpStaging.Spec.ProposedBranch, ctpStaging.Spec.ActiveBranch))
+				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpStaging.Spec.ProposedBranch, ctpStaging.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -225,7 +225,7 @@ var _ = Describe("PromotionStrategy Controller", func() {
 				g.Expect(err).To(HaveOccurred())
 				g.Expect(errors.IsNotFound(err)).To(BeTrue())
 
-				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
+				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -314,7 +314,7 @@ var _ = Describe("PromotionStrategy Controller", func() {
 
 			Eventually(func(g Gomega) {
 				// Dev PR should be closed because it is the lowest level environment
-				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpDev.Spec.ProposedBranch, ctpDev.Spec.ActiveBranch))
+				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpDev.Spec.ProposedBranch, ctpDev.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -322,14 +322,14 @@ var _ = Describe("PromotionStrategy Controller", func() {
 				g.Expect(err).To(HaveOccurred())
 				g.Expect(errors.IsNotFound(err)).To(BeTrue())
 
-				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpStaging.Spec.ProposedBranch, ctpStaging.Spec.ActiveBranch))
+				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpStaging.Spec.ProposedBranch, ctpStaging.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
 				}, &pullRequestStaging)
 				g.Expect(err).To(Succeed())
 
-				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
+				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -369,7 +369,7 @@ var _ = Describe("PromotionStrategy Controller", func() {
 
 			By("By checking that the staging pull request has been merged and the production pull request is still open")
 			Eventually(func(g Gomega) {
-				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpStaging.Spec.ProposedBranch, ctpStaging.Spec.ActiveBranch))
+				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpStaging.Spec.ProposedBranch, ctpStaging.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -377,7 +377,7 @@ var _ = Describe("PromotionStrategy Controller", func() {
 				g.Expect(err).To(HaveOccurred())
 				g.Expect(errors.IsNotFound(err)).To(BeTrue())
 
-				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
+				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -431,7 +431,7 @@ var _ = Describe("PromotionStrategy Controller", func() {
 
 			By("By checking that the production pull request has been merged")
 			Eventually(func(g Gomega) {
-				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
+				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -512,21 +512,21 @@ var _ = Describe("PromotionStrategy Controller", func() {
 				g.Expect(err).To(Succeed())
 
 				// Dev PR should stay open because it has a proposed commit
-				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpDev.Spec.ProposedBranch, ctpDev.Spec.ActiveBranch))
+				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpDev.Spec.ProposedBranch, ctpDev.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
 				}, &pullRequestDev)
 				g.Expect(err).To(Succeed())
 
-				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpStaging.Spec.ProposedBranch, ctpStaging.Spec.ActiveBranch))
+				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpStaging.Spec.ProposedBranch, ctpStaging.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
 				}, &pullRequestStaging)
 				g.Expect(err).To(Succeed())
 
-				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
+				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -566,7 +566,7 @@ var _ = Describe("PromotionStrategy Controller", func() {
 
 			By("By checking that the development pull request has been merged and that staging, production pull request are still open")
 			Eventually(func(g Gomega) {
-				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpDev.Spec.ProposedBranch, ctpDev.Spec.ActiveBranch))
+				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpDev.Spec.ProposedBranch, ctpDev.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -574,14 +574,14 @@ var _ = Describe("PromotionStrategy Controller", func() {
 				g.Expect(err).To(HaveOccurred())
 				g.Expect(errors.IsNotFound(err)).To(BeTrue())
 
-				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpStaging.Spec.ProposedBranch, ctpStaging.Spec.ActiveBranch))
+				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpStaging.Spec.ProposedBranch, ctpStaging.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
 				}, &pullRequestStaging)
 				g.Expect(err).To(Succeed())
 
-				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
+				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -700,7 +700,7 @@ var _ = Describe("PromotionStrategy Controller", func() {
 			pullRequestProd := promoterv1alpha1.PullRequest{}
 			Eventually(func(g Gomega) {
 				// Dev PR should be closed because it is the lowest level environment
-				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpDev.Spec.ProposedBranch, ctpDev.Spec.ActiveBranch))
+				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpDev.Spec.ProposedBranch, ctpDev.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -708,14 +708,14 @@ var _ = Describe("PromotionStrategy Controller", func() {
 				g.Expect(err).To(HaveOccurred())
 				g.Expect(errors.IsNotFound(err)).To(BeTrue())
 
-				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpStaging.Spec.ProposedBranch, ctpStaging.Spec.ActiveBranch))
+				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpStaging.Spec.ProposedBranch, ctpStaging.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
 				}, &pullRequestStaging)
 				g.Expect(err).To(Succeed())
 
-				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
+				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -742,7 +742,7 @@ var _ = Describe("PromotionStrategy Controller", func() {
 				err = k8sClient.Update(ctx, &argoCDAppDev)
 				Expect(err).To(Succeed())
 
-				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpStaging.Spec.ProposedBranch, ctpStaging.Spec.ActiveBranch))
+				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpStaging.Spec.ProposedBranch, ctpStaging.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -750,7 +750,7 @@ var _ = Describe("PromotionStrategy Controller", func() {
 				g.Expect(err).To(HaveOccurred())
 				g.Expect(errors.IsNotFound(err)).To(BeTrue())
 
-				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
+				prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -786,7 +786,7 @@ var _ = Describe("PromotionStrategy Controller", func() {
 				err = k8sClient.Update(ctx, &argoCDAppStaging)
 				Expect(err).To(Succeed())
 
-				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
+				prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctpProd.Spec.ProposedBranch, ctpProd.Spec.ActiveBranch))
 				err = k8sClient.Get(ctx, types.NamespacedName{
 					Name:      prName,
 					Namespace: typeNamespacedName.Namespace,
@@ -837,8 +837,10 @@ func promotionStrategyResource(ctx context.Context, name, namespace string) (str
 			Namespace: namespace,
 		},
 		Spec: promoterv1alpha1.GitRepositorySpec{
-			Owner: name,
-			Name:  name,
+			Fake: &promoterv1alpha1.FakeRepo{
+				Owner: name,
+				Name:  name,
+			},
 			ScmProviderRef: promoterv1alpha1.ObjectReference{
 				Name: name,
 			},
diff --git a/internal/controller/pullrequest_controller.go b/internal/controller/pullrequest_controller.go
index 792b25c0b..1105b8b68 100644
--- a/internal/controller/pullrequest_controller.go
+++ b/internal/controller/pullrequest_controller.go
@@ -26,6 +26,7 @@ import (
 	"github.com/argoproj-labs/gitops-promoter/internal/scms"
 	"github.com/argoproj-labs/gitops-promoter/internal/scms/fake"
 	"github.com/argoproj-labs/gitops-promoter/internal/scms/github"
+	"github.com/argoproj-labs/gitops-promoter/internal/scms/gitlab"
 	"github.com/argoproj-labs/gitops-promoter/internal/utils"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -196,6 +197,13 @@ func (r *PullRequestReconciler) getPullRequestProvider(ctx context.Context, pr p
 			return nil, fmt.Errorf("failed to get GitHub provider for domain %q and secret %q: %w", scmProvider.Spec.GitHub.Domain, secret.Name, err)
 		}
 		return p, nil
+	case scmProvider.Spec.GitLab != nil:
+		var p *gitlab.PullRequest
+		p, err = gitlab.NewGitlabPullRequestProvider(r.Client, *secret, scmProvider.Spec.GitLab.Domain)
+		if err != nil {
+			return nil, fmt.Errorf("failed to get GitLab provider for domain %q and secret %q: %w", scmProvider.Spec.GitLab.Domain, secret.Name, err)
+		}
+		return p, nil
 	case scmProvider.Spec.Fake != nil:
 		return fake.NewFakePullRequestProvider(r.Client), nil
 	default:
diff --git a/internal/controller/pullrequest_controller_test.go b/internal/controller/pullrequest_controller_test.go
index 3e5f13932..e4dd4c207 100644
--- a/internal/controller/pullrequest_controller_test.go
+++ b/internal/controller/pullrequest_controller_test.go
@@ -150,8 +150,10 @@ func pullRequestResources(ctx context.Context, name, namespace string) (string,
 			Namespace: namespace,
 		},
 		Spec: promoterv1alpha1.GitRepositorySpec{
-			Owner: name,
-			Name:  name,
+			Fake: &promoterv1alpha1.FakeRepo{
+				Owner: name,
+				Name:  name,
+			},
 			ScmProviderRef: promoterv1alpha1.ObjectReference{
 				Name: name,
 			},
diff --git a/internal/scms/fake/git_operations.go b/internal/scms/fake/git_operations.go
index 9a8f865c6..183581e42 100644
--- a/internal/scms/fake/git_operations.go
+++ b/internal/scms/fake/git_operations.go
@@ -26,9 +26,9 @@ func (gh GitAuthenticationProvider) GetGitHttpsRepoUrl(gitRepo v1alpha1.GitRepos
 	gitServerPortStr := fmt.Sprintf("%d", gitServerPort)
 
 	if gh.scmProvider.Spec.Fake != nil && gh.scmProvider.Spec.Fake.Domain == "" {
-		return fmt.Sprintf("http://localhost:%s/%s/%s", gitServerPortStr, gitRepo.Spec.Owner, gitRepo.Spec.Name)
+		return fmt.Sprintf("http://localhost:%s/%s/%s", gitServerPortStr, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name)
 	}
-	return fmt.Sprintf("http://localhost:%s/%s/%s", gitServerPortStr, gitRepo.Spec.Owner, gitRepo.Spec.Name)
+	return fmt.Sprintf("http://localhost:%s/%s/%s", gitServerPortStr, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name)
 }
 
 func (gh GitAuthenticationProvider) GetToken(ctx context.Context) (string, error) {
diff --git a/internal/scms/fake/pullrequest.go b/internal/scms/fake/pullrequest.go
index 02beb26fe..8b454e40d 100644
--- a/internal/scms/fake/pullrequest.go
+++ b/internal/scms/fake/pullrequest.go
@@ -50,14 +50,14 @@ func (pr *PullRequest) Create(ctx context.Context, title, head, base, descriptio
 	}
 
 	pullRequestCopy := pullRequest.DeepCopy()
-	if p, ok := pullRequests[pr.getMapKey(*pullRequest, gitRepo.Spec.Owner, gitRepo.Spec.Name)]; ok {
+	if p, ok := pullRequests[pr.getMapKey(*pullRequest, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name)]; ok {
 		logger.Info("Pull request already exists", "id", p.ID)
 	}
 	if pullRequestCopy == nil {
 		return "", errors.New("pull request is nil")
 	}
 
-	pullRequests[pr.getMapKey(*pullRequestCopy, gitRepo.Spec.Owner, gitRepo.Spec.Name)] = PullRequestProviderState{
+	pullRequests[pr.getMapKey(*pullRequestCopy, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name)] = PullRequestProviderState{
 		ID:    fmt.Sprintf("%d", len(pullRequests)+1),
 		State: v1alpha1.PullRequestOpen,
 	}
@@ -78,7 +78,7 @@ func (pr *PullRequest) Close(ctx context.Context, pullRequest *v1alpha1.PullRequ
 		return fmt.Errorf("failed to get GitRepository: %w", err)
 	}
 
-	prKey := pr.getMapKey(*pullRequest, gitRepo.Spec.Owner, gitRepo.Spec.Name)
+	prKey := pr.getMapKey(*pullRequest, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name)
 	if _, ok := pullRequests[prKey]; !ok {
 		return errors.New("pull request not found")
 	}
@@ -110,7 +110,7 @@ func (pr *PullRequest) Merge(ctx context.Context, commitMessage string, pullRequ
 
 	gitServerPort := 5000 + ginkgov2.GinkgoParallelProcess()
 	gitServerPortStr := fmt.Sprintf("%d", gitServerPort)
-	err = pr.runGitCmd(gitPath, "clone", "--verbose", "--progress", "--filter=blob:none", "-b", pullRequest.Spec.TargetBranch, fmt.Sprintf("http://localhost:%s/%s/%s", gitServerPortStr, gitRepo.Spec.Owner, gitRepo.Spec.Name), ".")
+	err = pr.runGitCmd(gitPath, "clone", "--verbose", "--progress", "--filter=blob:none", "-b", pullRequest.Spec.TargetBranch, fmt.Sprintf("http://localhost:%s/%s/%s", gitServerPortStr, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name), ".")
 	if err != nil {
 		return err
 	}
@@ -130,7 +130,7 @@ func (pr *PullRequest) Merge(ctx context.Context, commitMessage string, pullRequ
 	}
 
 	mutexPR.Lock()
-	prKey := pr.getMapKey(*pullRequest, gitRepo.Spec.Owner, gitRepo.Spec.Name)
+	prKey := pr.getMapKey(*pullRequest, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name)
 
 	if _, ok := pullRequests[prKey]; !ok {
 		return errors.New("pull request not found")
@@ -162,7 +162,7 @@ func (pr *PullRequest) findOpen(ctx context.Context, pullRequest *v1alpha1.PullR
 		return false
 	}
 
-	pullRequestState, ok := pullRequests[pr.getMapKey(*pullRequest, gitRepo.Spec.Owner, gitRepo.Spec.Name)]
+	pullRequestState, ok := pullRequests[pr.getMapKey(*pullRequest, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name)]
 	return ok && pullRequestState.State == v1alpha1.PullRequestOpen
 }
 
diff --git a/internal/scms/github/commit_status.go b/internal/scms/github/commit_status.go
index aa2e9da9d..4f947e092 100644
--- a/internal/scms/github/commit_status.go
+++ b/internal/scms/github/commit_status.go
@@ -50,7 +50,7 @@ func (cs CommitStatus) Set(ctx context.Context, commitStatus *promoterv1alpha1.C
 		return nil, fmt.Errorf("failed to get GitRepository: %w", err)
 	}
 
-	repoStatus, response, err := cs.client.Repositories.CreateStatus(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, commitStatus.Spec.Sha, commitStatusS)
+	repoStatus, response, err := cs.client.Repositories.CreateStatus(ctx, gitRepo.Spec.GitHub.Owner, gitRepo.Spec.GitHub.Name, commitStatus.Spec.Sha, commitStatusS)
 	if err != nil {
 		return nil, fmt.Errorf("failed to create status: %w", err)
 	}
diff --git a/internal/scms/github/git_operations.go b/internal/scms/github/git_operations.go
index 61d628935..7a79f5d09 100644
--- a/internal/scms/github/git_operations.go
+++ b/internal/scms/github/git_operations.go
@@ -49,9 +49,9 @@ func NewGithubGitAuthenticationProvider(scmProvider *v1alpha1.ScmProvider, secre
 
 func (gh GitAuthenticationProvider) GetGitHttpsRepoUrl(gitRepository v1alpha1.GitRepository) string {
 	if gh.scmProvider.Spec.GitHub != nil && gh.scmProvider.Spec.GitHub.Domain != "" {
-		return fmt.Sprintf("https://git@%s/%s/%s.git", gh.scmProvider.Spec.GitHub.Domain, gitRepository.Spec.Owner, gitRepository.Spec.Name)
+		return fmt.Sprintf("https://git@%s/%s/%s.git", gh.scmProvider.Spec.GitHub.Domain, gitRepository.Spec.GitHub.Owner, gitRepository.Spec.GitHub.Name)
 	}
-	return fmt.Sprintf("https://git@github.com/%s/%s.git", gitRepository.Spec.Owner, gitRepository.Spec.Name)
+	return fmt.Sprintf("https://git@github.com/%s/%s.git", gitRepository.Spec.GitHub.Owner, gitRepository.Spec.GitHub.Name)
 }
 
 func (gh GitAuthenticationProvider) GetToken(ctx context.Context) (string, error) {
diff --git a/internal/scms/github/pullrequest.go b/internal/scms/github/pullrequest.go
index 623e1269a..a52e96caf 100644
--- a/internal/scms/github/pullrequest.go
+++ b/internal/scms/github/pullrequest.go
@@ -50,7 +50,7 @@ func (pr *PullRequest) Create(ctx context.Context, title, head, base, descriptio
 		return "", fmt.Errorf("failed to get GitRepository: %w", err)
 	}
 
-	githubPullRequest, response, err := pr.client.PullRequests.Create(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, newPR)
+	githubPullRequest, response, err := pr.client.PullRequests.Create(ctx, gitRepo.Spec.GitHub.Owner, gitRepo.Spec.GitHub.Name, newPR)
 	if err != nil {
 		return "", fmt.Errorf("failed to create pull request: %w", err)
 	}
@@ -79,7 +79,7 @@ func (pr *PullRequest) Update(ctx context.Context, title, description string, pu
 
 	gitRepo, _ := utils.GetGitRepositoryFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{Namespace: pullRequest.Namespace, Name: pullRequest.Spec.RepositoryReference.Name})
 
-	_, response, err := pr.client.PullRequests.Edit(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, prNumber, newPR)
+	_, response, err := pr.client.PullRequests.Edit(ctx, gitRepo.Spec.GitHub.Owner, gitRepo.Spec.GitHub.Name, prNumber, newPR)
 	if err != nil {
 		return fmt.Errorf("failed to edit pull request: %w", err)
 	}
@@ -111,7 +111,7 @@ func (pr *PullRequest) Close(ctx context.Context, pullRequest *v1alpha1.PullRequ
 		return fmt.Errorf("failed to get GitRepository: %w", err)
 	}
 
-	_, response, err := pr.client.PullRequests.Edit(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, prNumber, newPR)
+	_, response, err := pr.client.PullRequests.Edit(ctx, gitRepo.Spec.GitHub.Owner, gitRepo.Spec.GitHub.Name, prNumber, newPR)
 	if err != nil {
 		return fmt.Errorf("failed to close pull request: %w", err)
 	}
@@ -137,8 +137,8 @@ func (pr *PullRequest) Merge(ctx context.Context, commitMessage string, pullRequ
 
 	_, response, err := pr.client.PullRequests.Merge(
 		ctx,
-		gitRepo.Spec.Owner,
-		gitRepo.Spec.Name,
+		gitRepo.Spec.GitHub.Owner,
+		gitRepo.Spec.GitHub.Name,
 		prNumber,
 		commitMessage,
 		&github.PullRequestOptions{
@@ -165,8 +165,9 @@ func (pr *PullRequest) FindOpen(ctx context.Context, pullRequest *v1alpha1.PullR
 
 	gitRepo, _ := utils.GetGitRepositoryFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{Namespace: pullRequest.Namespace, Name: pullRequest.Spec.RepositoryReference.Name})
 
-	pullRequests, response, err := pr.client.PullRequests.List(ctx, gitRepo.Spec.Owner,
-		gitRepo.Spec.Name,
+	pullRequests, response, err := pr.client.PullRequests.List(
+		ctx, gitRepo.Spec.GitHub.Owner,
+		gitRepo.Spec.GitHub.Name,
 		&github.PullRequestListOptions{Base: pullRequest.Spec.TargetBranch, Head: pullRequest.Spec.SourceBranch, State: "open"})
 	if err != nil {
 		return false, fmt.Errorf("failed to list pull requests: %w", err)
diff --git a/internal/scms/gitlab/commit_status.go b/internal/scms/gitlab/commit_status.go
new file mode 100644
index 000000000..5b43c59a2
--- /dev/null
+++ b/internal/scms/gitlab/commit_status.go
@@ -0,0 +1,75 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	"github.com/argoproj-labs/gitops-promoter/internal/utils"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/argoproj-labs/gitops-promoter/internal/scms"
+	v1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+type CommitStatus struct {
+	client    *gitlab.Client
+	k8sClient client.Client
+}
+
+var _ scms.CommitStatusProvider = &CommitStatus{}
+
+func NewGitlabCommitStatusProvider(k8sClient client.Client, secret v1.Secret, domain string) (*CommitStatus, error) {
+	client, err := GetClient(secret, domain)
+	if err != nil {
+		return nil, err
+	}
+
+	return &CommitStatus{client: client, k8sClient: k8sClient}, nil
+}
+
+func (cs *CommitStatus) Set(ctx context.Context, commitStatus *v1alpha1.CommitStatus) (*v1alpha1.CommitStatus, error) {
+	logger := log.FromContext(ctx)
+	logger.Info("Setting Commit Phase")
+
+	repo, err := utils.GetGitRepositoryFromObjectKey(ctx, cs.k8sClient, client.ObjectKey{
+		Namespace: commitStatus.Namespace,
+		Name:      commitStatus.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("failed to get repo: %w", err)
+	}
+
+	commitStatusOptions := &gitlab.SetCommitStatusOptions{
+		State:       phaseToBuildState(commitStatus.Spec.Phase),
+		TargetURL:   gitlab.Ptr(commitStatus.Spec.Url),
+		Name:        gitlab.Ptr(commitStatus.Spec.Name),
+		Description: gitlab.Ptr(commitStatus.Spec.Description),
+	}
+
+	glStatus, resp, err := cs.client.Commits.SetCommitStatus(
+		repo.Spec.GitLab.ProjectID,
+		commitStatus.Spec.Sha,
+		commitStatusOptions,
+		gitlab.WithContext(ctx),
+	)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create status: %w", err)
+	}
+
+	logGitLabRateLimitsIfAvailable(
+		logger,
+		repo.Spec.ScmProviderRef.Name,
+		resp,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	commitStatus.Status.Id = strconv.Itoa(glStatus.ID)
+	commitStatus.Status.Phase = buildStateToPhase(gitlab.BuildStateValue(glStatus.Status))
+	commitStatus.Status.Sha = commitStatus.Spec.Sha
+	return commitStatus, nil
+}
diff --git a/internal/scms/gitlab/git_operations.go b/internal/scms/gitlab/git_operations.go
new file mode 100644
index 000000000..2516bbde3
--- /dev/null
+++ b/internal/scms/gitlab/git_operations.go
@@ -0,0 +1,70 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+	"net/url"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	v1 "k8s.io/api/core/v1"
+)
+
+type GitAuthenticationProvider struct {
+	scmProvider *v1alpha1.ScmProvider
+	secret      *v1.Secret
+	client      *gitlab.Client
+}
+
+func NewGitlabGitAuthenticationProvider(scmProvider *v1alpha1.ScmProvider, secret *v1.Secret) (*GitAuthenticationProvider, error) {
+	client, err := GetClient(*secret, scmProvider.Spec.GitLab.Domain)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create GitLab Client: %w", err)
+	}
+
+	return &GitAuthenticationProvider{
+		scmProvider: scmProvider,
+		secret:      secret,
+		client:      client,
+	}, nil
+}
+
+func (gl GitAuthenticationProvider) GetGitHttpsRepoUrl(repo v1alpha1.GitRepository) string {
+	var repoUrl string
+	if gl.scmProvider.Spec.GitLab != nil && gl.scmProvider.Spec.GitLab.Domain != "" {
+		repoUrl = fmt.Sprintf("https://%s/%s/%s.git", gl.scmProvider.Spec.GitLab.Domain, repo.Spec.GitLab.Namespace, repo.Spec.GitLab.Name)
+	} else {
+		repoUrl = fmt.Sprintf("https://gitlab.com/%s/%s.git", repo.Spec.GitLab.Namespace, repo.Spec.GitLab.Name)
+	}
+	if _, err := url.Parse(repoUrl); err != nil {
+		return ""
+	}
+	return repoUrl
+}
+
+func (gl GitAuthenticationProvider) GetToken(ctx context.Context) (string, error) {
+	return string(gl.secret.Data["token"]), nil
+}
+
+func (gl GitAuthenticationProvider) GetUser(ctx context.Context) (string, error) {
+	return "oauth2", nil
+}
+
+func GetClient(secret v1.Secret, domain string) (*gitlab.Client, error) {
+	token := string(secret.Data["token"])
+	if token == "" {
+		return nil, fmt.Errorf("secret %q is missing required data key 'token'", secret.Name)
+	}
+
+	opts := []gitlab.ClientOptionFunc{}
+	if domain != "" {
+		opts = append(opts, gitlab.WithBaseURL(fmt.Sprintf("https://%s/api/v4", domain)))
+	}
+
+	client, err := gitlab.NewClient(token, opts...)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create GitLab client: %w", err)
+	}
+
+	return client, nil
+}
diff --git a/internal/scms/gitlab/pullrequest.go b/internal/scms/gitlab/pullrequest.go
new file mode 100644
index 000000000..2ebf06694
--- /dev/null
+++ b/internal/scms/gitlab/pullrequest.go
@@ -0,0 +1,230 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	"github.com/argoproj-labs/gitops-promoter/internal/scms"
+	"github.com/argoproj-labs/gitops-promoter/internal/utils"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	v1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+type PullRequest struct {
+	client    *gitlab.Client
+	k8sClient client.Client
+}
+
+var _ scms.PullRequestProvider = &PullRequest{}
+
+func NewGitlabPullRequestProvider(k8sClient client.Client, secret v1.Secret, domain string) (*PullRequest, error) {
+	client, err := GetClient(secret, domain)
+	if err != nil {
+		return nil, err
+	}
+
+	return &PullRequest{
+		client:    client,
+		k8sClient: k8sClient,
+	}, nil
+}
+
+func (pr *PullRequest) Create(ctx context.Context, title, head, base, desc string, prObj *v1alpha1.PullRequest) (string, error) {
+	logger := log.FromContext(ctx)
+
+	repo, err := utils.GetGitRepositoryFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return "", fmt.Errorf("failed to get GitRepository: %w", err)
+	}
+
+	options := &gitlab.CreateMergeRequestOptions{
+		Title:        gitlab.Ptr(title),
+		SourceBranch: gitlab.Ptr(head),
+		TargetBranch: gitlab.Ptr(base),
+		Description:  gitlab.Ptr(desc),
+	}
+
+	mr, resp, err := pr.client.MergeRequests.CreateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		options,
+	)
+	if err != nil {
+		return "", fmt.Errorf("failed to create pull request: %w", err)
+	}
+
+	logGitLabRateLimitsIfAvailable(
+		logger,
+		prObj.Spec.RepositoryReference.Name,
+		resp,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return strconv.Itoa(mr.IID), nil
+}
+
+func (pr *PullRequest) Update(ctx context.Context, title, description string, prObj *v1alpha1.PullRequest) error {
+	logger := log.FromContext(ctx)
+
+	mrIID, err := strconv.Atoi(prObj.Status.ID)
+	if err != nil {
+		return fmt.Errorf("failed to convert MR ID %q to int: %w", prObj.Status.ID, err)
+	}
+
+	repo, err := utils.GetGitRepositoryFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to get repo: %w", err)
+	}
+
+	options := &gitlab.UpdateMergeRequestOptions{
+		Title:       gitlab.Ptr(title),
+		Description: gitlab.Ptr(description),
+	}
+
+	_, resp, err := pr.client.MergeRequests.UpdateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		mrIID,
+		options,
+		gitlab.WithContext(ctx),
+	)
+	if err != nil {
+		return fmt.Errorf("failed to update merge request: %w", err)
+	}
+
+	logGitLabRateLimitsIfAvailable(
+		logger,
+		prObj.Spec.RepositoryReference.Name,
+		resp,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return nil
+}
+
+func (pr *PullRequest) Close(ctx context.Context, prObj *v1alpha1.PullRequest) error {
+	logger := log.FromContext(ctx)
+
+	mrIID, err := strconv.Atoi(prObj.Status.ID)
+	if err != nil {
+		return fmt.Errorf("failed to convert MR ID %q to int: %w", prObj.Status.ID, err)
+	}
+
+	repo, err := utils.GetGitRepositoryFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to get repo: %w", err)
+	}
+
+	options := &gitlab.UpdateMergeRequestOptions{
+		StateEvent: gitlab.Ptr("close"),
+	}
+
+	_, resp, err := pr.client.MergeRequests.UpdateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		mrIID,
+		options,
+		gitlab.WithContext(ctx),
+	)
+	if err != nil {
+		return fmt.Errorf("failed to close merge request: %w", err)
+	}
+
+	logGitLabRateLimitsIfAvailable(
+		logger,
+		prObj.Spec.RepositoryReference.Name,
+		resp,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return nil
+}
+
+func (pr *PullRequest) Merge(ctx context.Context, commitMessage string, prObj *v1alpha1.PullRequest) error {
+	logger := log.FromContext(ctx)
+
+	mrIID, err := strconv.Atoi(prObj.Status.ID)
+	if err != nil {
+		return fmt.Errorf("failed to convert MR number to int: %w", err)
+	}
+
+	repo, err := utils.GetGitRepositoryFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to get repo: %w", err)
+	}
+
+	options := &gitlab.AcceptMergeRequestOptions{
+		MergeCommitMessage:        gitlab.Ptr(commitMessage),
+		MergeWhenPipelineSucceeds: gitlab.Ptr(false),
+		ShouldRemoveSourceBranch:  gitlab.Ptr(false),
+		Squash:                    gitlab.Ptr(false),
+	}
+
+	_, resp, err := pr.client.MergeRequests.AcceptMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		mrIID,
+		options,
+		gitlab.WithContext(ctx),
+	)
+	if err != nil {
+		return fmt.Errorf("failed to merge request: %w", err)
+	}
+
+	logGitLabRateLimitsIfAvailable(
+		logger,
+		prObj.Spec.RepositoryReference.Name,
+		resp,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return nil
+}
+
+func (pr *PullRequest) FindOpen(ctx context.Context, prObj *v1alpha1.PullRequest) (bool, error) {
+	logger := log.FromContext(ctx)
+	logger.V(4).Info("Finding Open Pull Request")
+
+	options := &gitlab.ListMergeRequestsOptions{
+		SourceBranch: gitlab.Ptr(prObj.Spec.SourceBranch),
+		TargetBranch: gitlab.Ptr(prObj.Spec.TargetBranch),
+		State:        gitlab.Ptr("opened"),
+	}
+
+	mrs, resp, err := pr.client.MergeRequests.ListMergeRequests(options)
+	if err != nil {
+		return false, fmt.Errorf("failed to list pull requests: %w", err)
+	}
+
+	logGitLabRateLimitsIfAvailable(
+		logger,
+		prObj.Spec.RepositoryReference.Name,
+		resp,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	if len(mrs) > 0 {
+		prObj.Status.ID = strconv.Itoa(mrs[0].IID)
+		prObj.Status.State = mapMergeRequestState(mrs[0].State)
+		return true, nil
+	}
+
+	return false, nil
+}
diff --git a/internal/scms/gitlab/utils.go b/internal/scms/gitlab/utils.go
new file mode 100644
index 000000000..0e0c6780c
--- /dev/null
+++ b/internal/scms/gitlab/utils.go
@@ -0,0 +1,60 @@
+package gitlab
+
+import (
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	"github.com/go-logr/logr"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+)
+
+func logGitLabRateLimitsIfAvailable(
+	logger logr.Logger,
+	scmProvider string,
+	resp *gitlab.Response,
+) {
+	limit := resp.Header.Get("Ratelimit-Limit")
+	remaining := resp.Header.Get("Ratelimit-Remaining")
+	reset := resp.Header.Get("Ratelimit-Reset")
+
+	if limit != "" || remaining != "" || reset != "" {
+		logger.Info("GitLab rate limits",
+			"scmProvider", scmProvider,
+			"limit", limit,
+			"remaining", remaining,
+			"reset", reset,
+			"url", resp.Request.URL.String(),
+		)
+	}
+}
+
+func mapMergeRequestState(gl string) v1alpha1.PullRequestState {
+	switch gl {
+	case "opened":
+		return v1alpha1.PullRequestOpen
+	case "closed":
+		return v1alpha1.PullRequestClosed
+	default:
+		return v1alpha1.PullRequestMerged
+	}
+}
+
+func phaseToBuildState(phase v1alpha1.CommitStatusPhase) gitlab.BuildStateValue {
+	switch phase {
+	case v1alpha1.CommitPhaseSuccess:
+		return gitlab.Success
+	case v1alpha1.CommitPhasePending:
+		return gitlab.Pending
+	default:
+		return gitlab.Failed
+	}
+}
+
+func buildStateToPhase(buildState gitlab.BuildStateValue) v1alpha1.CommitStatusPhase {
+	switch buildState {
+	case gitlab.Success:
+		return v1alpha1.CommitPhaseSuccess
+	case gitlab.Pending:
+		return v1alpha1.CommitPhasePending
+	default:
+		return v1alpha1.CommitPhaseFailure
+	}
+}
diff --git a/internal/utils/utils.go b/internal/utils/utils.go
index ee9497675..882153bbf 100644
--- a/internal/utils/utils.go
+++ b/internal/utils/utils.go
@@ -37,6 +37,12 @@ func GetScmProviderFromGitRepository(ctx context.Context, k8sClient client.Clien
 		return nil, fmt.Errorf("failed to get ScmProvider: %w", err)
 	}
 
+	if (repositoryRef.Spec.GitHub != nil && scmProvider.Spec.GitHub == nil) ||
+		(repositoryRef.Spec.GitLab != nil && scmProvider.Spec.GitLab == nil) ||
+		(repositoryRef.Spec.Fake != nil && scmProvider.Spec.Fake == nil) {
+		return nil, fmt.Errorf("wrong ScmProvider configured for Repository")
+	}
+
 	return &scmProvider, nil
 }
 


Reviews:
user: login: crenshaw-dev
body: Looking fantastic! Did a quick first round review.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-02T17:00:52Z
user: login: zachaller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T21:02:21Z
user: login: crenshaw-dev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T19:50:56Z
user: login: robinlieb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T20:09:10Z
user: login: robinlieb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T20:09:29Z
user: login: robinlieb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T20:10:11Z
user: login: robinlieb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T20:11:39Z
user: login: robinlieb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T20:12:06Z
user: login: robinlieb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T20:13:46Z
user: login: robinlieb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T20:14:49Z
user: login: crenshaw-dev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T20:29:04Z
user: login: crenshaw-dev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T20:29:24Z
user: login: robinlieb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T19:35:23Z
user: login: robinlieb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T19:37:08Z
user: login: crenshaw-dev
body: Let's heckin' go: https://gitlab.com/mac9416/gitops-promoter-tests/-/merge_requests
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T21:35:25Z
user: login: crenshaw-dev
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T19:27:34Z
user: login: zachaller
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T19:31:07Z

Review comments:
diff_hunk: @@ -25,10 +25,9 @@ import (
 
 // GitRepositorySpec defines the desired state of GitRepository
 type GitRepositorySpec struct {
-	// +kubebuilder:validation:Required
-	Owner string `json:"owner"`
-	// +kubebuilder:validation:Required
-	Name string `json:"name"`
+	GitHub GitHubRepo `json:"github,omitempty"`
+	GitLab GitLabRepo `json:"gitlab,omitempty"`
path: api/v1alpha1/gitrepository_types.go
user: login: crenshaw-dev
body: Maybe pointers so it's super easy to tell if one's meant to be used?
created_at: 2025-03-02T16:44:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,10 +25,9 @@ import (
 
 // GitRepositorySpec defines the desired state of GitRepository
 type GitRepositorySpec struct {
-	// +kubebuilder:validation:Required
-	Owner string `json:"owner"`
-	// +kubebuilder:validation:Required
-	Name string `json:"name"`
+	GitHub GitHubRepo `json:"github,omitempty"`
+	GitLab GitLabRepo `json:"gitlab,omitempty"`
+	Fake   FakeRepo   `json:"fake,omitempty"`
 	// +kubebuilder:validation:Required
 	ScmProviderRef ObjectReference `json:"scmProviderRef"`
path: api/v1alpha1/gitrepository_types.go
user: login: crenshaw-dev
body: We'll need new validation to throw an error if someone uses a repo from one SCM with the SCMProvider of another SCM.
created_at: 2025-03-02T16:45:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -340,7 +344,16 @@ func (r *ChangeTransferPolicyReconciler) creatOrUpdatePullRequest(ctx context.Co
 	if err != nil {
 		return fmt.Errorf("failed to get GitRepository %q: %w", ctp.Spec.RepositoryReference.Name, err)
 	}
-	prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctp.Spec.ProposedBranch, ctp.Spec.ActiveBranch))
+
+	var prName string
+	switch {
+	case gitRepo.Spec.GitHub.Name != "":
+		prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.GitHub.Owner, gitRepo.Spec.GitHub.Name, ctp.Spec.ProposedBranch, ctp.Spec.ActiveBranch))
+	case gitRepo.Spec.GitLab.Name != "":
+		prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.GitLab.Namespace, gitRepo.Spec.GitLab.Name, ctp.Spec.ProposedBranch, ctp.Spec.ActiveBranch))
+	case gitRepo.Spec.Fake.Name != "":
+		prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctp.Spec.ProposedBranch, ctp.Spec.ActiveBranch))
+	}
path: internal/controller/changetransferpolicy_controller.go
user: login: crenshaw-dev
body: Could probably extract the KubeSafeUniqueName call instead of duplicating.
created_at: 2025-03-02T16:47:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -310,7 +310,7 @@ func (g *GitOperations) runCmd(ctx context.Context, directory string, args ...st
 	cmd := exec.Command("git", args...)
 	cmd.Env = []string{
 		"GIT_ASKPASS=promoter_askpass.sh", // Needs to be on path
-		fmt.Sprintf("GIT_USER=%s", user),
+		fmt.Sprintf("GIT_USERNAME=%s", user),
path: internal/git/git.go
user: login: crenshaw-dev
body: Was this previously just broken? If so, might justify its own PR/test.
created_at: 2025-03-02T16:48:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,97 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	"github.com/argoproj-labs/gitops-promoter/internal/utils"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/argoproj-labs/gitops-promoter/internal/scms"
+	v1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+type CommitStatus struct {
+	client    *gitlab.Client
+	k8sClient client.Client
+}
+
+var _ scms.CommitStatusProvider = &CommitStatus{}
+
+func NewGitlabCommitStatusProvider(k8sClient client.Client, secret v1.Secret, domain string) (*CommitStatus, error) {
+	client, err := GetClient(secret, domain)
+	if err != nil {
+		return nil, err
+	}
+
+	return &CommitStatus{client: client, k8sClient: k8sClient}, nil
+}
+
+func (cs *CommitStatus) Set(ctx context.Context, commitStatus *v1alpha1.CommitStatus) (*v1alpha1.CommitStatus, error) {
+	logger := log.FromContext(ctx)
+	logger.Info("Setting Commit Phase")
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, cs.k8sClient, client.ObjectKey{
path: internal/scms/gitlab/commit_status.go
user: login: crenshaw-dev
body: ```suggestion
	repo, err := utils.GetGitRepositoryFromObjectKey(ctx, cs.k8sClient, client.ObjectKey{
```

Not sure if this is a new typo or was already there
created_at: 2025-03-02T16:49:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,97 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	"github.com/argoproj-labs/gitops-promoter/internal/utils"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/argoproj-labs/gitops-promoter/internal/scms"
+	v1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+type CommitStatus struct {
+	client    *gitlab.Client
+	k8sClient client.Client
+}
+
+var _ scms.CommitStatusProvider = &CommitStatus{}
+
+func NewGitlabCommitStatusProvider(k8sClient client.Client, secret v1.Secret, domain string) (*CommitStatus, error) {
+	client, err := GetClient(secret, domain)
+	if err != nil {
+		return nil, err
+	}
+
+	return &CommitStatus{client: client, k8sClient: k8sClient}, nil
+}
+
+func (cs *CommitStatus) Set(ctx context.Context, commitStatus *v1alpha1.CommitStatus) (*v1alpha1.CommitStatus, error) {
+	logger := log.FromContext(ctx)
+	logger.Info("Setting Commit Phase")
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, cs.k8sClient, client.ObjectKey{
+		Namespace: commitStatus.Namespace,
+		Name:      commitStatus.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("failed to get repo: %w", err)
+	}
+
+	commitStatusOptions := &gitlab.SetCommitStatusOptions{
+		State:       phaseToBuildState(commitStatus.Spec.Phase),
+		TargetURL:   gitlab.Ptr(commitStatus.Spec.Url),
+		Name:        gitlab.Ptr(commitStatus.Spec.Name),
+		Description: gitlab.Ptr(commitStatus.Spec.Description),
+	}
+
+	glStatus, resp, err := cs.client.Commits.SetCommitStatus(repo.Spec.GitLab.ProjectID,
+		commitStatus.Spec.Sha,
+		commitStatusOptions,
+		gitlab.WithContext(ctx))
+
+	if err != nil {
+		return nil, fmt.Errorf("failed to create status: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
path: internal/scms/gitlab/commit_status.go
user: login: crenshaw-dev
body: Is rate limiting done on a per-token basis? If so, can we make sure we log something like the ScmProvider's name so we can correlate those two?
created_at: 2025-03-02T16:51:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	v1 "k8s.io/api/core/v1"
+)
+
+type GitAuthenticationProvider struct {
+	scmProvider *v1alpha1.ScmProvider
+	secret      *v1.Secret
+	client      *gitlab.Client
+}
+
+func NewGitlabGitAuthenticationProvider(scmProvider *v1alpha1.ScmProvider, secret *v1.Secret) GitAuthenticationProvider {
+	client, err := GetClient(*secret, scmProvider.Spec.GitLab.Domain)
+	if err != nil {
+		panic(err)
path: internal/scms/gitlab/git_operations.go
user: login: crenshaw-dev
body: Could we just return up the error?
created_at: 2025-03-02T16:52:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	v1 "k8s.io/api/core/v1"
+)
+
+type GitAuthenticationProvider struct {
+	scmProvider *v1alpha1.ScmProvider
+	secret      *v1.Secret
+	client      *gitlab.Client
+}
+
+func NewGitlabGitAuthenticationProvider(scmProvider *v1alpha1.ScmProvider, secret *v1.Secret) GitAuthenticationProvider {
+	client, err := GetClient(*secret, scmProvider.Spec.GitLab.Domain)
+	if err != nil {
+		panic(err)
+	}
+
+	return GitAuthenticationProvider{
+		scmProvider: scmProvider,
+		secret:      secret,
+		client:      client,
+	}
+}
+
+func (gl GitAuthenticationProvider) GetGitHttpsRepoUrl(repo v1alpha1.GitRepository) string {
+	if gl.scmProvider.Spec.GitLab != nil && gl.scmProvider.Spec.GitLab.Domain != "" {
+		return fmt.Sprintf("https://%s/%s/%s.git", gl.scmProvider.Spec.GitLab.Domain, repo.Spec.GitLab.Namespace, repo.Spec.GitLab.Name)
+	}
path: internal/scms/gitlab/git_operations.go
user: login: crenshaw-dev
body: We should probably do some validation on these URL parts.
created_at: 2025-03-02T16:53:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,236 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	"github.com/argoproj-labs/gitops-promoter/internal/scms"
+	"github.com/argoproj-labs/gitops-promoter/internal/utils"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	v1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+type PullRequest struct {
+	client    *gitlab.Client
+	k8sClient client.Client
+}
+
+var _ scms.PullRequestProvider = &PullRequest{}
+
+func NewGitlabPullRequestProvider(k8sClient client.Client, secret v1.Secret, domain string) (*PullRequest, error) {
+	client, err := GetClient(secret, domain)
+	if err != nil {
+		return nil, err
+	}
+
+	return &PullRequest{
+		client:    client,
+		k8sClient: k8sClient,
+	}, nil
+}
+
+func (pr *PullRequest) Create(ctx context.Context, title, head, base, desc string, prObj *v1alpha1.PullRequest) (string, error) {
+	logger := log.FromContext(ctx)
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return "", fmt.Errorf("failed to get GitRepository: %w", err)
+	}
+
+	options := &gitlab.CreateMergeRequestOptions{
+		Title:        gitlab.Ptr(title),
+		SourceBranch: gitlab.Ptr(head),
+		TargetBranch: gitlab.Ptr(base),
+		Description:  gitlab.Ptr(desc),
+	}
+
+	mr, resp, err := pr.client.MergeRequests.CreateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		options,
+	)
+	if err != nil {
+		return "", fmt.Errorf("failed to create pull request: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
path: internal/scms/gitlab/pullrequest.go
user: login: crenshaw-dev
body: Ditto re: logging a field to identify the token.
created_at: 2025-03-02T16:56:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,236 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	"github.com/argoproj-labs/gitops-promoter/internal/scms"
+	"github.com/argoproj-labs/gitops-promoter/internal/utils"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	v1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+type PullRequest struct {
+	client    *gitlab.Client
+	k8sClient client.Client
+}
+
+var _ scms.PullRequestProvider = &PullRequest{}
+
+func NewGitlabPullRequestProvider(k8sClient client.Client, secret v1.Secret, domain string) (*PullRequest, error) {
+	client, err := GetClient(secret, domain)
+	if err != nil {
+		return nil, err
+	}
+
+	return &PullRequest{
+		client:    client,
+		k8sClient: k8sClient,
+	}, nil
+}
+
+func (pr *PullRequest) Create(ctx context.Context, title, head, base, desc string, prObj *v1alpha1.PullRequest) (string, error) {
+	logger := log.FromContext(ctx)
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return "", fmt.Errorf("failed to get GitRepository: %w", err)
+	}
+
+	options := &gitlab.CreateMergeRequestOptions{
+		Title:        gitlab.Ptr(title),
+		SourceBranch: gitlab.Ptr(head),
+		TargetBranch: gitlab.Ptr(base),
+		Description:  gitlab.Ptr(desc),
+	}
+
+	mr, resp, err := pr.client.MergeRequests.CreateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		options,
+	)
+	if err != nil {
+		return "", fmt.Errorf("failed to create pull request: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return strconv.Itoa(mr.IID), nil
+}
+
+func (pr *PullRequest) Update(ctx context.Context, title, description string, prObj *v1alpha1.PullRequest) error {
+	logger := log.FromContext(ctx)
+
+	mrIID, err := strconv.Atoi(prObj.Status.ID)
path: internal/scms/gitlab/pullrequest.go
user: login: crenshaw-dev
body: I wonder if we should standardize on "Merge Request" within the context of the gitlab-specific code for variable names, etc.
created_at: 2025-03-02T16:57:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,236 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	"github.com/argoproj-labs/gitops-promoter/internal/scms"
+	"github.com/argoproj-labs/gitops-promoter/internal/utils"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	v1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+type PullRequest struct {
+	client    *gitlab.Client
+	k8sClient client.Client
+}
+
+var _ scms.PullRequestProvider = &PullRequest{}
+
+func NewGitlabPullRequestProvider(k8sClient client.Client, secret v1.Secret, domain string) (*PullRequest, error) {
+	client, err := GetClient(secret, domain)
+	if err != nil {
+		return nil, err
+	}
+
+	return &PullRequest{
+		client:    client,
+		k8sClient: k8sClient,
+	}, nil
+}
+
+func (pr *PullRequest) Create(ctx context.Context, title, head, base, desc string, prObj *v1alpha1.PullRequest) (string, error) {
+	logger := log.FromContext(ctx)
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return "", fmt.Errorf("failed to get GitRepository: %w", err)
+	}
+
+	options := &gitlab.CreateMergeRequestOptions{
+		Title:        gitlab.Ptr(title),
+		SourceBranch: gitlab.Ptr(head),
+		TargetBranch: gitlab.Ptr(base),
+		Description:  gitlab.Ptr(desc),
+	}
+
+	mr, resp, err := pr.client.MergeRequests.CreateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		options,
+	)
+	if err != nil {
+		return "", fmt.Errorf("failed to create pull request: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return strconv.Itoa(mr.IID), nil
+}
+
+func (pr *PullRequest) Update(ctx context.Context, title, description string, prObj *v1alpha1.PullRequest) error {
+	logger := log.FromContext(ctx)
+
+	mrIID, err := strconv.Atoi(prObj.Status.ID)
+	if err != nil {
+		return fmt.Errorf("invalid merge request ID: %w", err)
+	}
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to get repo: %w", err)
+	}
+
+	options := &gitlab.UpdateMergeRequestOptions{
+		Title:       gitlab.Ptr(title),
+		Description: gitlab.Ptr(description),
+	}
+
+	_, resp, err := pr.client.MergeRequests.UpdateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		mrIID,
+		options,
+		gitlab.WithContext(ctx),
+	)
+	if err != nil {
+		return fmt.Errorf("failed to update merge request: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return nil
+}
+
+func (pr *PullRequest) Close(ctx context.Context, prObj *v1alpha1.PullRequest) error {
+	logger := log.FromContext(ctx)
+
+	mrIID, err := strconv.Atoi(prObj.Status.ID)
+	if err != nil {
+		return fmt.Errorf("invalid merge request ID: %w", err)
path: internal/scms/gitlab/pullrequest.go
user: login: crenshaw-dev
body: ```suggestion
		return fmt.Errorf("invalid merge request ID: %q", err)
```

Maybe? Just in case it's an empty string
created_at: 2025-03-02T16:58:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,236 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	"github.com/argoproj-labs/gitops-promoter/internal/scms"
+	"github.com/argoproj-labs/gitops-promoter/internal/utils"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	v1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+type PullRequest struct {
+	client    *gitlab.Client
+	k8sClient client.Client
+}
+
+var _ scms.PullRequestProvider = &PullRequest{}
+
+func NewGitlabPullRequestProvider(k8sClient client.Client, secret v1.Secret, domain string) (*PullRequest, error) {
+	client, err := GetClient(secret, domain)
+	if err != nil {
+		return nil, err
+	}
+
+	return &PullRequest{
+		client:    client,
+		k8sClient: k8sClient,
+	}, nil
+}
+
+func (pr *PullRequest) Create(ctx context.Context, title, head, base, desc string, prObj *v1alpha1.PullRequest) (string, error) {
+	logger := log.FromContext(ctx)
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return "", fmt.Errorf("failed to get GitRepository: %w", err)
+	}
+
+	options := &gitlab.CreateMergeRequestOptions{
+		Title:        gitlab.Ptr(title),
+		SourceBranch: gitlab.Ptr(head),
+		TargetBranch: gitlab.Ptr(base),
+		Description:  gitlab.Ptr(desc),
+	}
+
+	mr, resp, err := pr.client.MergeRequests.CreateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		options,
+	)
+	if err != nil {
+		return "", fmt.Errorf("failed to create pull request: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return strconv.Itoa(mr.IID), nil
+}
+
+func (pr *PullRequest) Update(ctx context.Context, title, description string, prObj *v1alpha1.PullRequest) error {
+	logger := log.FromContext(ctx)
+
+	mrIID, err := strconv.Atoi(prObj.Status.ID)
+	if err != nil {
+		return fmt.Errorf("invalid merge request ID: %w", err)
+	}
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to get repo: %w", err)
+	}
+
+	options := &gitlab.UpdateMergeRequestOptions{
+		Title:       gitlab.Ptr(title),
+		Description: gitlab.Ptr(description),
+	}
+
+	_, resp, err := pr.client.MergeRequests.UpdateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		mrIID,
+		options,
+		gitlab.WithContext(ctx),
+	)
+	if err != nil {
+		return fmt.Errorf("failed to update merge request: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return nil
+}
+
+func (pr *PullRequest) Close(ctx context.Context, prObj *v1alpha1.PullRequest) error {
+	logger := log.FromContext(ctx)
+
+	mrIID, err := strconv.Atoi(prObj.Status.ID)
+	if err != nil {
+		return fmt.Errorf("invalid merge request ID: %w", err)
+	}
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to get repo: %w", err)
+	}
+
+	options := &gitlab.UpdateMergeRequestOptions{
+		StateEvent: gitlab.Ptr("close"),
+	}
+
+	_, resp, err := pr.client.MergeRequests.UpdateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		mrIID,
+		options,
+		gitlab.WithContext(ctx),
+	)
+	if err != nil {
+		return fmt.Errorf("failed to close merge request: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
path: internal/scms/gitlab/pullrequest.go
user: login: crenshaw-dev
body: Will stop +1'ing the extra field request, but will say here that it probably makes sense to pull a utility function for adding rate limiting fields.
created_at: 2025-03-02T16:59:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,236 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	"github.com/argoproj-labs/gitops-promoter/internal/scms"
+	"github.com/argoproj-labs/gitops-promoter/internal/utils"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	v1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+type PullRequest struct {
+	client    *gitlab.Client
+	k8sClient client.Client
+}
+
+var _ scms.PullRequestProvider = &PullRequest{}
+
+func NewGitlabPullRequestProvider(k8sClient client.Client, secret v1.Secret, domain string) (*PullRequest, error) {
+	client, err := GetClient(secret, domain)
+	if err != nil {
+		return nil, err
+	}
+
+	return &PullRequest{
+		client:    client,
+		k8sClient: k8sClient,
+	}, nil
+}
+
+func (pr *PullRequest) Create(ctx context.Context, title, head, base, desc string, prObj *v1alpha1.PullRequest) (string, error) {
+	logger := log.FromContext(ctx)
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return "", fmt.Errorf("failed to get GitRepository: %w", err)
+	}
+
+	options := &gitlab.CreateMergeRequestOptions{
+		Title:        gitlab.Ptr(title),
+		SourceBranch: gitlab.Ptr(head),
+		TargetBranch: gitlab.Ptr(base),
+		Description:  gitlab.Ptr(desc),
+	}
+
+	mr, resp, err := pr.client.MergeRequests.CreateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		options,
+	)
+	if err != nil {
+		return "", fmt.Errorf("failed to create pull request: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return strconv.Itoa(mr.IID), nil
+}
+
+func (pr *PullRequest) Update(ctx context.Context, title, description string, prObj *v1alpha1.PullRequest) error {
+	logger := log.FromContext(ctx)
+
+	mrIID, err := strconv.Atoi(prObj.Status.ID)
+	if err != nil {
+		return fmt.Errorf("invalid merge request ID: %w", err)
+	}
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to get repo: %w", err)
+	}
+
+	options := &gitlab.UpdateMergeRequestOptions{
+		Title:       gitlab.Ptr(title),
+		Description: gitlab.Ptr(description),
+	}
+
+	_, resp, err := pr.client.MergeRequests.UpdateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		mrIID,
+		options,
+		gitlab.WithContext(ctx),
+	)
+	if err != nil {
+		return fmt.Errorf("failed to update merge request: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return nil
+}
+
+func (pr *PullRequest) Close(ctx context.Context, prObj *v1alpha1.PullRequest) error {
+	logger := log.FromContext(ctx)
+
+	mrIID, err := strconv.Atoi(prObj.Status.ID)
+	if err != nil {
+		return fmt.Errorf("invalid merge request ID: %w", err)
+	}
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to get repo: %w", err)
+	}
+
+	options := &gitlab.UpdateMergeRequestOptions{
+		StateEvent: gitlab.Ptr("close"),
+	}
+
+	_, resp, err := pr.client.MergeRequests.UpdateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		mrIID,
+		options,
+		gitlab.WithContext(ctx),
+	)
+	if err != nil {
+		return fmt.Errorf("failed to close merge request: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return nil
+}
+
+func (pr *PullRequest) Merge(ctx context.Context, commitMessage string, prObj *v1alpha1.PullRequest) error {
+	logger := log.FromContext(ctx)
+
+	mrIID, err := strconv.Atoi(prObj.Status.ID)
+	if err != nil {
+		return fmt.Errorf("invalid merge request ID: %w", err)
+	}
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to get repo: %w", err)
+	}
+
+	options := &gitlab.AcceptMergeRequestOptions{
+		MergeCommitMessage:        gitlab.Ptr(commitMessage),
+		MergeWhenPipelineSucceeds: gitlab.Ptr(false),
+		ShouldRemoveSourceBranch:  gitlab.Ptr(false),
+		Squash:                    gitlab.Ptr(false),
+	}
+
+	_, resp, err := pr.client.MergeRequests.AcceptMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		mrIID,
+		options,
+		gitlab.WithContext(ctx),
+	)
+	if err != nil {
+		return fmt.Errorf("failed to merge request: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return nil
+}
+
+func (pr *PullRequest) FindOpen(ctx context.Context, prObj *v1alpha1.PullRequest) (bool, error) {
+	logger := log.FromContext(ctx)
+	logger.V(4).Info("Finding Open Pull Request")
+
+	options := &gitlab.ListMergeRequestsOptions{
+		SourceBranch: gitlab.Ptr(prObj.Spec.SourceBranch),
+		TargetBranch: gitlab.Ptr(prObj.Spec.TargetBranch),
+		State:        gitlab.Ptr("opened"),
+	}
+
+	mrs, resp, err := pr.client.MergeRequests.ListMergeRequests(options)
+	if err != nil {
+		return false, fmt.Errorf("failed to list pull requests: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	if len(mrs) > 0 {
+		prObj.Status.ID = strconv.Itoa(mrs[0].IID)
+		prObj.Status.State = v1alpha1.PullRequestState(strings.TrimSuffix(mrs[0].State, "ed"))
path: internal/scms/gitlab/pullrequest.go
user: login: crenshaw-dev
body: Might be worth documenting why the "ed" strip works. Or possibly switching to an explicit switch statement, if that's more readable.
created_at: 2025-03-02T17:00:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -310,7 +310,7 @@ func (g *GitOperations) runCmd(ctx context.Context, directory string, args ...st
 	cmd := exec.Command("git", args...)
 	cmd.Env = []string{
 		"GIT_ASKPASS=promoter_askpass.sh", // Needs to be on path
-		fmt.Sprintf("GIT_USER=%s", user),
+		fmt.Sprintf("GIT_USERNAME=%s", user),
path: internal/git/git.go
user: login: zachaller
body: The askpass script wants GIT_USERNAME https://github.com/argoproj-labs/gitops-promoter/blob/main/hack/git/promoter_askpass.sh#L5 
created_at: 2025-03-03T21:02:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -310,7 +310,7 @@ func (g *GitOperations) runCmd(ctx context.Context, directory string, args ...st
 	cmd := exec.Command("git", args...)
 	cmd.Env = []string{
 		"GIT_ASKPASS=promoter_askpass.sh", // Needs to be on path
-		fmt.Sprintf("GIT_USER=%s", user),
+		fmt.Sprintf("GIT_USERNAME=%s", user),
path: internal/git/git.go
user: login: crenshaw-dev
body: Fixed in a different PR
created_at: 2025-03-05T19:50:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,10 +25,9 @@ import (
 
 // GitRepositorySpec defines the desired state of GitRepository
 type GitRepositorySpec struct {
-	// +kubebuilder:validation:Required
-	Owner string `json:"owner"`
-	// +kubebuilder:validation:Required
-	Name string `json:"name"`
+	GitHub GitHubRepo `json:"github,omitempty"`
+	GitLab GitLabRepo `json:"gitlab,omitempty"`
path: api/v1alpha1/gitrepository_types.go
user: login: robinlieb
body: Updated, thanks!
created_at: 2025-03-11T20:09:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -340,7 +344,16 @@ func (r *ChangeTransferPolicyReconciler) creatOrUpdatePullRequest(ctx context.Co
 	if err != nil {
 		return fmt.Errorf("failed to get GitRepository %q: %w", ctp.Spec.RepositoryReference.Name, err)
 	}
-	prName := utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Owner, gitRepo.Spec.Name, ctp.Spec.ProposedBranch, ctp.Spec.ActiveBranch))
+
+	var prName string
+	switch {
+	case gitRepo.Spec.GitHub.Name != "":
+		prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.GitHub.Owner, gitRepo.Spec.GitHub.Name, ctp.Spec.ProposedBranch, ctp.Spec.ActiveBranch))
+	case gitRepo.Spec.GitLab.Name != "":
+		prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.GitLab.Namespace, gitRepo.Spec.GitLab.Name, ctp.Spec.ProposedBranch, ctp.Spec.ActiveBranch))
+	case gitRepo.Spec.Fake.Name != "":
+		prName = utils.KubeSafeUniqueName(ctx, utils.GetPullRequestName(ctx, gitRepo.Spec.Fake.Owner, gitRepo.Spec.Fake.Name, ctp.Spec.ProposedBranch, ctp.Spec.ActiveBranch))
+	}
path: internal/controller/changetransferpolicy_controller.go
user: login: robinlieb
body: Updated, thanks!
created_at: 2025-03-11T20:09:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,97 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	"github.com/argoproj-labs/gitops-promoter/internal/utils"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/argoproj-labs/gitops-promoter/internal/scms"
+	v1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+type CommitStatus struct {
+	client    *gitlab.Client
+	k8sClient client.Client
+}
+
+var _ scms.CommitStatusProvider = &CommitStatus{}
+
+func NewGitlabCommitStatusProvider(k8sClient client.Client, secret v1.Secret, domain string) (*CommitStatus, error) {
+	client, err := GetClient(secret, domain)
+	if err != nil {
+		return nil, err
+	}
+
+	return &CommitStatus{client: client, k8sClient: k8sClient}, nil
+}
+
+func (cs *CommitStatus) Set(ctx context.Context, commitStatus *v1alpha1.CommitStatus) (*v1alpha1.CommitStatus, error) {
+	logger := log.FromContext(ctx)
+	logger.Info("Setting Commit Phase")
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, cs.k8sClient, client.ObjectKey{
+		Namespace: commitStatus.Namespace,
+		Name:      commitStatus.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("failed to get repo: %w", err)
+	}
+
+	commitStatusOptions := &gitlab.SetCommitStatusOptions{
+		State:       phaseToBuildState(commitStatus.Spec.Phase),
+		TargetURL:   gitlab.Ptr(commitStatus.Spec.Url),
+		Name:        gitlab.Ptr(commitStatus.Spec.Name),
+		Description: gitlab.Ptr(commitStatus.Spec.Description),
+	}
+
+	glStatus, resp, err := cs.client.Commits.SetCommitStatus(repo.Spec.GitLab.ProjectID,
+		commitStatus.Spec.Sha,
+		commitStatusOptions,
+		gitlab.WithContext(ctx))
+
+	if err != nil {
+		return nil, fmt.Errorf("failed to create status: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
path: internal/scms/gitlab/commit_status.go
user: login: robinlieb
body: Updated, thanks!
created_at: 2025-03-11T20:10:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,236 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	"github.com/argoproj-labs/gitops-promoter/internal/scms"
+	"github.com/argoproj-labs/gitops-promoter/internal/utils"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	v1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+type PullRequest struct {
+	client    *gitlab.Client
+	k8sClient client.Client
+}
+
+var _ scms.PullRequestProvider = &PullRequest{}
+
+func NewGitlabPullRequestProvider(k8sClient client.Client, secret v1.Secret, domain string) (*PullRequest, error) {
+	client, err := GetClient(secret, domain)
+	if err != nil {
+		return nil, err
+	}
+
+	return &PullRequest{
+		client:    client,
+		k8sClient: k8sClient,
+	}, nil
+}
+
+func (pr *PullRequest) Create(ctx context.Context, title, head, base, desc string, prObj *v1alpha1.PullRequest) (string, error) {
+	logger := log.FromContext(ctx)
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return "", fmt.Errorf("failed to get GitRepository: %w", err)
+	}
+
+	options := &gitlab.CreateMergeRequestOptions{
+		Title:        gitlab.Ptr(title),
+		SourceBranch: gitlab.Ptr(head),
+		TargetBranch: gitlab.Ptr(base),
+		Description:  gitlab.Ptr(desc),
+	}
+
+	mr, resp, err := pr.client.MergeRequests.CreateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		options,
+	)
+	if err != nil {
+		return "", fmt.Errorf("failed to create pull request: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return strconv.Itoa(mr.IID), nil
+}
+
+func (pr *PullRequest) Update(ctx context.Context, title, description string, prObj *v1alpha1.PullRequest) error {
+	logger := log.FromContext(ctx)
+
+	mrIID, err := strconv.Atoi(prObj.Status.ID)
+	if err != nil {
+		return fmt.Errorf("invalid merge request ID: %w", err)
+	}
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to get repo: %w", err)
+	}
+
+	options := &gitlab.UpdateMergeRequestOptions{
+		Title:       gitlab.Ptr(title),
+		Description: gitlab.Ptr(description),
+	}
+
+	_, resp, err := pr.client.MergeRequests.UpdateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		mrIID,
+		options,
+		gitlab.WithContext(ctx),
+	)
+	if err != nil {
+		return fmt.Errorf("failed to update merge request: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return nil
+}
+
+func (pr *PullRequest) Close(ctx context.Context, prObj *v1alpha1.PullRequest) error {
+	logger := log.FromContext(ctx)
+
+	mrIID, err := strconv.Atoi(prObj.Status.ID)
+	if err != nil {
+		return fmt.Errorf("invalid merge request ID: %w", err)
+	}
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to get repo: %w", err)
+	}
+
+	options := &gitlab.UpdateMergeRequestOptions{
+		StateEvent: gitlab.Ptr("close"),
+	}
+
+	_, resp, err := pr.client.MergeRequests.UpdateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		mrIID,
+		options,
+		gitlab.WithContext(ctx),
+	)
+	if err != nil {
+		return fmt.Errorf("failed to close merge request: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return nil
+}
+
+func (pr *PullRequest) Merge(ctx context.Context, commitMessage string, prObj *v1alpha1.PullRequest) error {
+	logger := log.FromContext(ctx)
+
+	mrIID, err := strconv.Atoi(prObj.Status.ID)
+	if err != nil {
+		return fmt.Errorf("invalid merge request ID: %w", err)
+	}
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to get repo: %w", err)
+	}
+
+	options := &gitlab.AcceptMergeRequestOptions{
+		MergeCommitMessage:        gitlab.Ptr(commitMessage),
+		MergeWhenPipelineSucceeds: gitlab.Ptr(false),
+		ShouldRemoveSourceBranch:  gitlab.Ptr(false),
+		Squash:                    gitlab.Ptr(false),
+	}
+
+	_, resp, err := pr.client.MergeRequests.AcceptMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		mrIID,
+		options,
+		gitlab.WithContext(ctx),
+	)
+	if err != nil {
+		return fmt.Errorf("failed to merge request: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return nil
+}
+
+func (pr *PullRequest) FindOpen(ctx context.Context, prObj *v1alpha1.PullRequest) (bool, error) {
+	logger := log.FromContext(ctx)
+	logger.V(4).Info("Finding Open Pull Request")
+
+	options := &gitlab.ListMergeRequestsOptions{
+		SourceBranch: gitlab.Ptr(prObj.Spec.SourceBranch),
+		TargetBranch: gitlab.Ptr(prObj.Spec.TargetBranch),
+		State:        gitlab.Ptr("opened"),
+	}
+
+	mrs, resp, err := pr.client.MergeRequests.ListMergeRequests(options)
+	if err != nil {
+		return false, fmt.Errorf("failed to list pull requests: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	if len(mrs) > 0 {
+		prObj.Status.ID = strconv.Itoa(mrs[0].IID)
+		prObj.Status.State = v1alpha1.PullRequestState(strings.TrimSuffix(mrs[0].State, "ed"))
path: internal/scms/gitlab/pullrequest.go
user: login: robinlieb
body: Created util function to make it more explicit, thanks!
created_at: 2025-03-11T20:11:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	v1 "k8s.io/api/core/v1"
+)
+
+type GitAuthenticationProvider struct {
+	scmProvider *v1alpha1.ScmProvider
+	secret      *v1.Secret
+	client      *gitlab.Client
+}
+
+func NewGitlabGitAuthenticationProvider(scmProvider *v1alpha1.ScmProvider, secret *v1.Secret) GitAuthenticationProvider {
+	client, err := GetClient(*secret, scmProvider.Spec.GitLab.Domain)
+	if err != nil {
+		panic(err)
path: internal/scms/gitlab/git_operations.go
user: login: robinlieb
body: Done, thanks!
created_at: 2025-03-11T20:12:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,10 +25,9 @@ import (
 
 // GitRepositorySpec defines the desired state of GitRepository
 type GitRepositorySpec struct {
-	// +kubebuilder:validation:Required
-	Owner string `json:"owner"`
-	// +kubebuilder:validation:Required
-	Name string `json:"name"`
+	GitHub GitHubRepo `json:"github,omitempty"`
+	GitLab GitLabRepo `json:"gitlab,omitempty"`
+	Fake   FakeRepo   `json:"fake,omitempty"`
 	// +kubebuilder:validation:Required
 	ScmProviderRef ObjectReference `json:"scmProviderRef"`
path: api/v1alpha1/gitrepository_types.go
user: login: robinlieb
body: Added a check [here](https://github.com/argoproj-labs/gitops-promoter/pull/205/files#diff-a7cadd8b7616e655fd258138320b33eb96f7cce8e8663ec92651f49ce07df000R40), is this the way you had in mind?
created_at: 2025-03-11T20:13:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	v1 "k8s.io/api/core/v1"
+)
+
+type GitAuthenticationProvider struct {
+	scmProvider *v1alpha1.ScmProvider
+	secret      *v1.Secret
+	client      *gitlab.Client
+}
+
+func NewGitlabGitAuthenticationProvider(scmProvider *v1alpha1.ScmProvider, secret *v1.Secret) GitAuthenticationProvider {
+	client, err := GetClient(*secret, scmProvider.Spec.GitLab.Domain)
+	if err != nil {
+		panic(err)
+	}
+
+	return GitAuthenticationProvider{
+		scmProvider: scmProvider,
+		secret:      secret,
+		client:      client,
+	}
+}
+
+func (gl GitAuthenticationProvider) GetGitHttpsRepoUrl(repo v1alpha1.GitRepository) string {
+	if gl.scmProvider.Spec.GitLab != nil && gl.scmProvider.Spec.GitLab.Domain != "" {
+		return fmt.Sprintf("https://%s/%s/%s.git", gl.scmProvider.Spec.GitLab.Domain, repo.Spec.GitLab.Namespace, repo.Spec.GitLab.Name)
+	}
path: internal/scms/gitlab/git_operations.go
user: login: robinlieb
body: Just generally if its a valid url or do you have something specific in mind?
created_at: 2025-03-11T20:14:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	v1 "k8s.io/api/core/v1"
+)
+
+type GitAuthenticationProvider struct {
+	scmProvider *v1alpha1.ScmProvider
+	secret      *v1.Secret
+	client      *gitlab.Client
+}
+
+func NewGitlabGitAuthenticationProvider(scmProvider *v1alpha1.ScmProvider, secret *v1.Secret) GitAuthenticationProvider {
+	client, err := GetClient(*secret, scmProvider.Spec.GitLab.Domain)
+	if err != nil {
+		panic(err)
+	}
+
+	return GitAuthenticationProvider{
+		scmProvider: scmProvider,
+		secret:      secret,
+		client:      client,
+	}
+}
+
+func (gl GitAuthenticationProvider) GetGitHttpsRepoUrl(repo v1alpha1.GitRepository) string {
+	if gl.scmProvider.Spec.GitLab != nil && gl.scmProvider.Spec.GitLab.Domain != "" {
+		return fmt.Sprintf("https://%s/%s/%s.git", gl.scmProvider.Spec.GitLab.Domain, repo.Spec.GitLab.Namespace, repo.Spec.GitLab.Name)
+	}
path: internal/scms/gitlab/git_operations.go
user: login: crenshaw-dev
body: I think each part should be independently validated. The domain can probably be validated with the `url` package. The other two parts probably have to match these rules: https://docs.gitlab.com/user/reserved_names/

And then the whole URL should be assembled using the `url` package instead of just string formatting.
created_at: 2025-03-11T20:29:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,10 +25,9 @@ import (
 
 // GitRepositorySpec defines the desired state of GitRepository
 type GitRepositorySpec struct {
-	// +kubebuilder:validation:Required
-	Owner string `json:"owner"`
-	// +kubebuilder:validation:Required
-	Name string `json:"name"`
+	GitHub GitHubRepo `json:"github,omitempty"`
+	GitLab GitLabRepo `json:"gitlab,omitempty"`
+	Fake   FakeRepo   `json:"fake,omitempty"`
 	// +kubebuilder:validation:Required
 	ScmProviderRef ObjectReference `json:"scmProviderRef"`
path: api/v1alpha1/gitrepository_types.go
user: login: crenshaw-dev
body: Yep, that looks good!
created_at: 2025-03-11T20:29:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	v1 "k8s.io/api/core/v1"
+)
+
+type GitAuthenticationProvider struct {
+	scmProvider *v1alpha1.ScmProvider
+	secret      *v1.Secret
+	client      *gitlab.Client
+}
+
+func NewGitlabGitAuthenticationProvider(scmProvider *v1alpha1.ScmProvider, secret *v1.Secret) GitAuthenticationProvider {
+	client, err := GetClient(*secret, scmProvider.Spec.GitLab.Domain)
+	if err != nil {
+		panic(err)
+	}
+
+	return GitAuthenticationProvider{
+		scmProvider: scmProvider,
+		secret:      secret,
+		client:      client,
+	}
+}
+
+func (gl GitAuthenticationProvider) GetGitHttpsRepoUrl(repo v1alpha1.GitRepository) string {
+	if gl.scmProvider.Spec.GitLab != nil && gl.scmProvider.Spec.GitLab.Domain != "" {
+		return fmt.Sprintf("https://%s/%s/%s.git", gl.scmProvider.Spec.GitLab.Domain, repo.Spec.GitLab.Namespace, repo.Spec.GitLab.Name)
+	}
path: internal/scms/gitlab/git_operations.go
user: login: robinlieb
body: Added pattern validation for the `Namespace` and `Name` properties based on the provided GitLab documentation .
Also added a check if parsing of the constructed url is successful. If not, the `GetGitHttpsRepoUrl` returns empty string. Would probably better to return an error for proper error handling, but would go beyond the scope of this PR in my perspective. Would work on that on a dedicated PR if possible.

created_at: 2025-03-19T19:35:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,236 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	"github.com/argoproj-labs/gitops-promoter/internal/scms"
+	"github.com/argoproj-labs/gitops-promoter/internal/utils"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	v1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+type PullRequest struct {
+	client    *gitlab.Client
+	k8sClient client.Client
+}
+
+var _ scms.PullRequestProvider = &PullRequest{}
+
+func NewGitlabPullRequestProvider(k8sClient client.Client, secret v1.Secret, domain string) (*PullRequest, error) {
+	client, err := GetClient(secret, domain)
+	if err != nil {
+		return nil, err
+	}
+
+	return &PullRequest{
+		client:    client,
+		k8sClient: k8sClient,
+	}, nil
+}
+
+func (pr *PullRequest) Create(ctx context.Context, title, head, base, desc string, prObj *v1alpha1.PullRequest) (string, error) {
+	logger := log.FromContext(ctx)
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return "", fmt.Errorf("failed to get GitRepository: %w", err)
+	}
+
+	options := &gitlab.CreateMergeRequestOptions{
+		Title:        gitlab.Ptr(title),
+		SourceBranch: gitlab.Ptr(head),
+		TargetBranch: gitlab.Ptr(base),
+		Description:  gitlab.Ptr(desc),
+	}
+
+	mr, resp, err := pr.client.MergeRequests.CreateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		options,
+	)
+	if err != nil {
+		return "", fmt.Errorf("failed to create pull request: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return strconv.Itoa(mr.IID), nil
+}
+
+func (pr *PullRequest) Update(ctx context.Context, title, description string, prObj *v1alpha1.PullRequest) error {
+	logger := log.FromContext(ctx)
+
+	mrIID, err := strconv.Atoi(prObj.Status.ID)
path: internal/scms/gitlab/pullrequest.go
user: login: robinlieb
body: Do you mean that it's currently e.g. `repo` vs. `gitRepo`?
created_at: 2025-03-19T19:37:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -394,6 +395,13 @@ func (r *ArgoCDCommitStatusReconciler) getGitAuthProvider(ctx context.Context, a
 	case scmProvider.Spec.GitHub != nil:
 		logger.V(4).Info("Creating GitHub git authentication provider")
 		return github.NewGithubGitAuthenticationProvider(scmProvider, secret), ps.Spec.RepositoryReference, nil
+	case scmProvider.Spec.GitLab != nil:
+		logger.V(4).Info("Creating GitLab git authentication provider")
+		gitlabClient, err := gitlab.NewGitlabGitAuthenticationProvider(scmProvider, secret)
+		if err != nil {
+			return nil, ps.Spec.RepositoryReference, fmt.Errorf("failed to create Git Client: %w", err)
path: internal/controller/argocdcommitstatus_controller.go
user: login: crenshaw-dev
body: ```suggestion
			return nil, ps.Spec.RepositoryReference, fmt.Errorf("failed to create GitLab client: %w", err)
```
created_at: 2025-03-22T20:16:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -175,6 +176,13 @@ func (r *ChangeTransferPolicyReconciler) getGitAuthProvider(ctx context.Context,
 	case scmProvider.Spec.GitHub != nil:
 		logger.V(4).Info("Creating GitHub git authentication provider")
 		return github.NewGithubGitAuthenticationProvider(scmProvider, secret), nil
+	case scmProvider.Spec.GitLab != nil:
+		logger.V(4).Info("Creating GitLab git authentication provider")
+		provider, err := gitlab.NewGitlabGitAuthenticationProvider(scmProvider, secret)
+		if err != nil {
+			return nil, fmt.Errorf("failed to create Auth Provider: %w", err)
path: internal/controller/changetransferpolicy_controller.go
user: login: crenshaw-dev
body: ```suggestion
			return nil, fmt.Errorf("failed to create GitLab Auth Provider: %w", err)
```
created_at: 2025-03-22T20:16:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,70 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+	"net/url"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	v1 "k8s.io/api/core/v1"
+)
+
+type GitAuthenticationProvider struct {
+	scmProvider *v1alpha1.ScmProvider
+	secret      *v1.Secret
+	client      *gitlab.Client
+}
+
+func NewGitlabGitAuthenticationProvider(scmProvider *v1alpha1.ScmProvider, secret *v1.Secret) (*GitAuthenticationProvider, error) {
+	client, err := GetClient(*secret, scmProvider.Spec.GitLab.Domain)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create GitLab Client: %w", err)
+	}
+
+	return &GitAuthenticationProvider{
+		scmProvider: scmProvider,
+		secret:      secret,
+		client:      client,
+	}, nil
+}
+
+func (gl GitAuthenticationProvider) GetGitHttpsRepoUrl(repo v1alpha1.GitRepository) string {
+	var repoUrl string
+	if gl.scmProvider.Spec.GitLab != nil && gl.scmProvider.Spec.GitLab.Domain != "" {
+		repoUrl = fmt.Sprintf("https://%s/%s/%s.git", gl.scmProvider.Spec.GitLab.Domain, repo.Spec.GitLab.Namespace, repo.Spec.GitLab.Name)
+	} else {
+		repoUrl = fmt.Sprintf("https://gitlab.com/%s/%s.git", repo.Spec.GitLab.Namespace, repo.Spec.GitLab.Name)
+	}
+	if _, err := url.Parse(repoUrl); err != nil {
+		return ""
+	}
+	return repoUrl
+}
+
+func (gl GitAuthenticationProvider) GetToken(ctx context.Context) (string, error) {
+	return string(gl.secret.Data["token"]), nil
+}
+
+func (gl GitAuthenticationProvider) GetUser(ctx context.Context) (string, error) {
+	return "oauth2", nil
+}
+
+func GetClient(secret v1.Secret, domain string) (*gitlab.Client, error) {
+	token := string(secret.Data["token"])
+	if token == "" {
+		return nil, fmt.Errorf("GitLab token is required")
path: internal/scms/gitlab/git_operations.go
user: login: crenshaw-dev
body: ```suggestion
		return nil, fmt.Errorf("secret %q is missing required data key 'token'", secret.Name)
```
created_at: 2025-03-22T20:18:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,236 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	"github.com/argoproj-labs/gitops-promoter/internal/scms"
+	"github.com/argoproj-labs/gitops-promoter/internal/utils"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	v1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+type PullRequest struct {
+	client    *gitlab.Client
+	k8sClient client.Client
+}
+
+var _ scms.PullRequestProvider = &PullRequest{}
+
+func NewGitlabPullRequestProvider(k8sClient client.Client, secret v1.Secret, domain string) (*PullRequest, error) {
+	client, err := GetClient(secret, domain)
+	if err != nil {
+		return nil, err
+	}
+
+	return &PullRequest{
+		client:    client,
+		k8sClient: k8sClient,
+	}, nil
+}
+
+func (pr *PullRequest) Create(ctx context.Context, title, head, base, desc string, prObj *v1alpha1.PullRequest) (string, error) {
+	logger := log.FromContext(ctx)
+
+	repo, err := utils.GetGitRepositorytFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return "", fmt.Errorf("failed to get GitRepository: %w", err)
+	}
+
+	options := &gitlab.CreateMergeRequestOptions{
+		Title:        gitlab.Ptr(title),
+		SourceBranch: gitlab.Ptr(head),
+		TargetBranch: gitlab.Ptr(base),
+		Description:  gitlab.Ptr(desc),
+	}
+
+	mr, resp, err := pr.client.MergeRequests.CreateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		options,
+	)
+	if err != nil {
+		return "", fmt.Errorf("failed to create pull request: %w", err)
+	}
+
+	logger.Info("GitLab rate limits",
+		"limit", resp.Header.Get("Ratelimit-Limit"),
+		"remaining", resp.Header.Get("Ratelimit-Remaining"),
+		"reset", resp.Header.Get("Ratelimit-Reset"),
+		"url", resp.Request.URL,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return strconv.Itoa(mr.IID), nil
+}
+
+func (pr *PullRequest) Update(ctx context.Context, title, description string, prObj *v1alpha1.PullRequest) error {
+	logger := log.FromContext(ctx)
+
+	mrIID, err := strconv.Atoi(prObj.Status.ID)
path: internal/scms/gitlab/pullrequest.go
user: login: crenshaw-dev
body: nah, it just that most stuff in the new gitlab code refers to "pull requests" even though gitlab calls them "merge requests." Not a big deal, but maybe better to stick with the SCM's convention.
created_at: 2025-03-22T20:19:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,230 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	"github.com/argoproj-labs/gitops-promoter/internal/scms"
+	"github.com/argoproj-labs/gitops-promoter/internal/utils"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	v1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+type PullRequest struct {
+	client    *gitlab.Client
+	k8sClient client.Client
+}
+
+var _ scms.PullRequestProvider = &PullRequest{}
+
+func NewGitlabPullRequestProvider(k8sClient client.Client, secret v1.Secret, domain string) (*PullRequest, error) {
+	client, err := GetClient(secret, domain)
+	if err != nil {
+		return nil, err
+	}
+
+	return &PullRequest{
+		client:    client,
+		k8sClient: k8sClient,
+	}, nil
+}
+
+func (pr *PullRequest) Create(ctx context.Context, title, head, base, desc string, prObj *v1alpha1.PullRequest) (string, error) {
+	logger := log.FromContext(ctx)
+
+	repo, err := utils.GetGitRepositoryFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return "", fmt.Errorf("failed to get GitRepository: %w", err)
+	}
+
+	options := &gitlab.CreateMergeRequestOptions{
+		Title:        gitlab.Ptr(title),
+		SourceBranch: gitlab.Ptr(head),
+		TargetBranch: gitlab.Ptr(base),
+		Description:  gitlab.Ptr(desc),
+	}
+
+	mr, resp, err := pr.client.MergeRequests.CreateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		options,
+	)
+	if err != nil {
+		return "", fmt.Errorf("failed to create pull request: %w", err)
+	}
+
+	logGitLabRatelimits(
+		logger,
+		prObj.Spec.RepositoryReference.Name,
+		resp,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return strconv.Itoa(mr.IID), nil
+}
+
+func (pr *PullRequest) Update(ctx context.Context, title, description string, prObj *v1alpha1.PullRequest) error {
+	logger := log.FromContext(ctx)
+
+	mrIID, err := strconv.Atoi(prObj.Status.ID)
+	if err != nil {
+		return fmt.Errorf("failed to convert MR number to int: %w", err)
path: internal/scms/gitlab/pullrequest.go
user: login: crenshaw-dev
body: ```suggestion
		return fmt.Errorf("failed to convert MR ID %q to int: %w", prObj.Status.ID, err)
```
created_at: 2025-03-22T20:20:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,230 @@
+package gitlab
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+
+	"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
+	"github.com/argoproj-labs/gitops-promoter/internal/scms"
+	"github.com/argoproj-labs/gitops-promoter/internal/utils"
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	v1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+type PullRequest struct {
+	client    *gitlab.Client
+	k8sClient client.Client
+}
+
+var _ scms.PullRequestProvider = &PullRequest{}
+
+func NewGitlabPullRequestProvider(k8sClient client.Client, secret v1.Secret, domain string) (*PullRequest, error) {
+	client, err := GetClient(secret, domain)
+	if err != nil {
+		return nil, err
+	}
+
+	return &PullRequest{
+		client:    client,
+		k8sClient: k8sClient,
+	}, nil
+}
+
+func (pr *PullRequest) Create(ctx context.Context, title, head, base, desc string, prObj *v1alpha1.PullRequest) (string, error) {
+	logger := log.FromContext(ctx)
+
+	repo, err := utils.GetGitRepositoryFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return "", fmt.Errorf("failed to get GitRepository: %w", err)
+	}
+
+	options := &gitlab.CreateMergeRequestOptions{
+		Title:        gitlab.Ptr(title),
+		SourceBranch: gitlab.Ptr(head),
+		TargetBranch: gitlab.Ptr(base),
+		Description:  gitlab.Ptr(desc),
+	}
+
+	mr, resp, err := pr.client.MergeRequests.CreateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		options,
+	)
+	if err != nil {
+		return "", fmt.Errorf("failed to create pull request: %w", err)
+	}
+
+	logGitLabRatelimits(
+		logger,
+		prObj.Spec.RepositoryReference.Name,
+		resp,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return strconv.Itoa(mr.IID), nil
+}
+
+func (pr *PullRequest) Update(ctx context.Context, title, description string, prObj *v1alpha1.PullRequest) error {
+	logger := log.FromContext(ctx)
+
+	mrIID, err := strconv.Atoi(prObj.Status.ID)
+	if err != nil {
+		return fmt.Errorf("failed to convert MR number to int: %w", err)
+	}
+
+	repo, err := utils.GetGitRepositoryFromObjectKey(ctx, pr.k8sClient, client.ObjectKey{
+		Namespace: prObj.Namespace,
+		Name:      prObj.Spec.RepositoryReference.Name,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to get repo: %w", err)
+	}
+
+	options := &gitlab.UpdateMergeRequestOptions{
+		Title:       gitlab.Ptr(title),
+		Description: gitlab.Ptr(description),
+	}
+
+	_, resp, err := pr.client.MergeRequests.UpdateMergeRequest(
+		repo.Spec.GitLab.ProjectID,
+		mrIID,
+		options,
+		gitlab.WithContext(ctx),
+	)
+	if err != nil {
+		return fmt.Errorf("failed to update merge request: %w", err)
+	}
+
+	logGitLabRatelimits(
+		logger,
+		prObj.Spec.RepositoryReference.Name,
+		resp,
+	)
+	logger.V(4).Info("github response status",
+		"status", resp.Status)
+
+	return nil
+}
+
+func (pr *PullRequest) Close(ctx context.Context, prObj *v1alpha1.PullRequest) error {
+	logger := log.FromContext(ctx)
+
+	mrIID, err := strconv.Atoi(prObj.Status.ID)
+	if err != nil {
+		return fmt.Errorf("failed to convert MR number to int: %w", err)
path: internal/scms/gitlab/pullrequest.go
user: login: crenshaw-dev
body: ```suggestion
		return fmt.Errorf("failed to convert MR ID %q to int: %w", prObj.Status.ID, err)
```
created_at: 2025-03-22T20:20:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -97,6 +98,46 @@ spec:
     The GitRepository and ScmProvider also need to be installed to the same namespace that you plan on creating PromotionStrategy 
     resources in, and it also needs to be in the same namespace of the secret it references.
 
+## GitLab Configuration
+
+To configure the GitOps Promoter with GitLab, you will need to create a GitLab Access Token with `api` and `write_repository` scopes and configure the necessary resources to allow the promoter to interact with your repository. This Access Token should be used in a secret as follows:
path: docs/getting-started.md
user: login: crenshaw-dev
body: I had to use the Developer role. That seems to be the lowest role which allows write access to MRs and commit statuses.

```suggestion
To configure the GitOps Promoter with GitLab, you will need to create a GitLab Access Token under the "Developer" role with `api` and `write_repository` scopes and configure the necessary resources to allow the promoter to interact with your repository. This Access Token should be used in a secret as follows:
```
created_at: 2025-03-22T21:35:08Z
author_association: CONTRIBUTOR

PR comments:
user: login: zachaller
created_at: 2025-03-06T21:37:53Z
body: One thing to note and this is more information than anything that needs to be done in this PR, there is also webhook support that is somewhat required at lest for faster reconciliation triggering than the default I think 10 minutes. There is not much support for multiple providers but you can see that [here](https://github.com/argoproj-labs/gitops-promoter/blob/main/internal/webhookreceiver/server.go)
author_association: CONTRIBUTOR
user: login: codecov-commenter
created_at: 2025-03-11T20:15:11Z
body: ## [Codecov](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/pull/205?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs) Report
:x: Patch coverage is `9.43396%` with `48 lines` in your changes missing coverage. Please review.
:white_check_mark: Project coverage is 50.25%. Comparing base ([`c6a2c1b`](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/commit/c6a2c1ba505a2650e9bfb24aea5a4e00f847de78?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs)) to head ([`64e7641`](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/commit/64e764135ab71fa35ca41a30beb9835ce44ac50a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs)).
:warning: Report is 742 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/pull/205?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs) | Patch % | Lines |
|---|---|---|
| [...rnal/controller/changetransferpolicy\_controller.go](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/pull/205?src=pr&el=tree&filepath=internal%2Fcontroller%2Fchangetransferpolicy_controller.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs#diff-aW50ZXJuYWwvY29udHJvbGxlci9jaGFuZ2V0cmFuc2ZlcnBvbGljeV9jb250cm9sbGVyLmdv) | 31.25% | [11 Missing :warning: ](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/pull/205?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs) |
| [internal/scms/github/pullrequest.go](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/pull/205?src=pr&el=tree&filepath=internal%2Fscms%2Fgithub%2Fpullrequest.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs#diff-aW50ZXJuYWwvc2Ntcy9naXRodWIvcHVsbHJlcXVlc3QuZ28=) | 0.00% | [8 Missing :warning: ](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/pull/205?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs) |
| [...ternal/controller/argocdcommitstatus\_controller.go](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/pull/205?src=pr&el=tree&filepath=internal%2Fcontroller%2Fargocdcommitstatus_controller.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs#diff-aW50ZXJuYWwvY29udHJvbGxlci9hcmdvY2Rjb21taXRzdGF0dXNfY29udHJvbGxlci5nbw==) | 0.00% | [7 Missing :warning: ](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/pull/205?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs) |
| [internal/controller/commitstatus\_controller.go](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/pull/205?src=pr&el=tree&filepath=internal%2Fcontroller%2Fcommitstatus_controller.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs#diff-aW50ZXJuYWwvY29udHJvbGxlci9jb21taXRzdGF0dXNfY29udHJvbGxlci5nbw==) | 0.00% | [7 Missing :warning: ](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/pull/205?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs) |
| [internal/controller/pullrequest\_controller.go](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/pull/205?src=pr&el=tree&filepath=internal%2Fcontroller%2Fpullrequest_controller.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs#diff-aW50ZXJuYWwvY29udHJvbGxlci9wdWxscmVxdWVzdF9jb250cm9sbGVyLmdv) | 0.00% | [7 Missing :warning: ](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/pull/205?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs) |
| [internal/utils/utils.go](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/pull/205?src=pr&el=tree&filepath=internal%2Futils%2Futils.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs#diff-aW50ZXJuYWwvdXRpbHMvdXRpbHMuZ28=) | 0.00% | [5 Missing :warning: ](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/pull/205?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs) |
| [internal/scms/github/git\_operations.go](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/pull/205?src=pr&el=tree&filepath=internal%2Fscms%2Fgithub%2Fgit_operations.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs#diff-aW50ZXJuYWwvc2Ntcy9naXRodWIvZ2l0X29wZXJhdGlvbnMuZ28=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/pull/205?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs) |
| [internal/scms/github/commit\_status.go](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/pull/205?src=pr&el=tree&filepath=internal%2Fscms%2Fgithub%2Fcommit_status.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs#diff-aW50ZXJuYWwvc2Ntcy9naXRodWIvY29tbWl0X3N0YXR1cy5nbw==) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/pull/205?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs) |

<details><summary>Additional details and impacted files</summary>



```diff
@@            Coverage Diff             @@
##             main     #205      +/-   ##
==========================================
- Coverage   50.94%   50.25%   -0.70%     
==========================================
  Files          15       15              
  Lines        1751     1793      +42     
==========================================
+ Hits          892      901       +9     
- Misses        739      773      +34     
+ Partials      120      119       -1     
```
</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/argoproj-labs/gitops-promoter/pull/205?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj-labs).
author_association: NONE
user: login: robinlieb
created_at: 2025-03-19T19:50:23Z
body: @zachaller good catch, created #222 to keep track of that.
author_association: CONTRIBUTOR
user: login: crenshaw-dev
created_at: 2025-03-24T14:16:36Z
body: I'm not getting anything for rate limits:

```
2025-03-24T10:15:27.383156-04:00        INFO    GitLab rate limits      {"controller": "pullrequest", "controllerGroup": "promoter.argoproj.io", "controllerKind": "PullRequest", "PullRequest": {"name":"mac9416-gitops-promoter-tests-environment-dev-next-environment-dev-acfd9df0","namespace":"promoter-system"}, "namespace": "promoter-system", "name": "mac9416-gitops-promoter-tests-environment-dev-next-environment-dev-acfd9df0", "reconcileID": "d28b1c59-8dcf-4fcf-9a60-c935b5d1cdc7", "scmProvider": "gitlab", "limit": "", "remaining": "", "reset": "", "url": "https://gitlab.com/api/v4/merge_requests?source_branch=environment%2Fdev-next&state=opened&target_branch=environment%2Fdev"}
```
author_association: CONTRIBUTOR
user: login: robinlieb
created_at: 2025-03-27T10:02:49Z
body: > I'm not getting anything for rate limits:
> 
> ```
> 2025-03-24T10:15:27.383156-04:00        INFO    GitLab rate limits      {"controller": "pullrequest", "controllerGroup": "promoter.argoproj.io", "controllerKind": "PullRequest", "PullRequest": {"name":"mac9416-gitops-promoter-tests-environment-dev-next-environment-dev-acfd9df0","namespace":"promoter-system"}, "namespace": "promoter-system", "name": "mac9416-gitops-promoter-tests-environment-dev-next-environment-dev-acfd9df0", "reconcileID": "d28b1c59-8dcf-4fcf-9a60-c935b5d1cdc7", "scmProvider": "gitlab", "limit": "", "remaining": "", "reset": "", "url": "https://gitlab.com/api/v4/merge_requests?source_branch=environment%2Fdev-next&state=opened&target_branch=environment%2Fdev"}
> ```

As far as I understand they are only provided on GitLab Self-Managed and also only once the limit is reached. There is no information provided to get the current status of the rate limit. I changed the implementation to only log if they are provided in the response.
author_association: CONTRIBUTOR