Repository: crossplane-contrib/provider-gitlab
Pull request #210 (2025-07-03 15:05:58)

feat(project): Add push rules

<!--
Thank you for helping to improve Crossplane!

Please read through https://git.io/fj2m9 if this is your first time opening a
Crossplane pull request. Find us in https://slack.crossplane.io/messages/dev if
you need any help contributing.
-->

### Description of your changes

<!--
Briefly describe what this pull request does. Be sure to direct your reviewers'
attention to anything that needs special consideration.

We love pull requests that resolve an open Crossplane issue. If yours does, you
can uncomment the below line to indicate which issue your PR fixes, for example
"Fixes #500":

-->
This adds push rule support to the project API.

I have:

- [x] Read and followed Crossplane's [contribution process].
- [x] Run `make reviewable test` to ensure this PR is ready for review.

### How has this code been tested

Unit tests and manual tests.

[contribution process]: https://git.io/fj2m9



Patch:
diff --git a/apis/projects/v1alpha1/project_types.go b/apis/projects/v1alpha1/project_types.go
index 4ef1e67..def35aa 100644
--- a/apis/projects/v1alpha1/project_types.go
+++ b/apis/projects/v1alpha1/project_types.go
@@ -153,6 +153,20 @@ type ProjectParameters struct {
 	// +optional
 	AllowMergeOnSkippedPipeline *bool `json:"allowMergeOnSkippedPipeline,omitempty"`
 
+	// NOTE: ApprovalsBeforeMerge is deprecated and approval rules should be
+	// used instead which has its own API.
+	//
+	// TODO: Implement approval rules API, maybe as separate MR or as part of
+	// this kind.
+	//
+	// TODO: Mark ApprovalsBeforeMerge as deprecated but keep it and replace it
+	// internally with a call to the approval rules API.
+
+	// How many approvers should approve merge request by default.More actions
+	// To configure approval rules, see Merge request approvals API.
+	// +optional
+	ApprovalsBeforeMerge *int `json:"approvalsBeforeMerge,omitempty"`
+
 	// Auto-cancel pending pipelines. This isn’t a boolean, but enabled/disabled.
 	// +optional
 	AutoCancelPendingPipelines *string `json:"autoCancelPendingPipelines,omitempty"`
@@ -202,6 +216,13 @@ type ProjectParameters struct {
 	// +optional
 	ContainerExpirationPolicyAttributes *ContainerExpirationPolicyAttributes `json:"containerExpirationPolicyAttributes,omitempty"`
 
+	// Enable container registry for this project.
+	//
+	// Deprecated: Use ContainerRegistryAccessLevel.
+	//
+	// +optional
+	ContainerRegistryEnabled *bool `json:"containerRegistryEnabled,omitempty"`
+
 	// Set visibility of container registry.
 	// +optional
 	ContainerRegistryAccessLevel *AccessControlValue `json:"containerRegistryAccessLevel,omitempty"`
@@ -340,6 +361,13 @@ type ProjectParameters struct {
 	// +optional
 	PublicBuilds *bool `json:"publicBuilds,omitempty"`
 
+	// PushRule specifies are pre-receive Git hooks you can enable in a
+	// user-friendly interface.
+	// Push rules give you more control over what can and can’t be pushed to
+	// your repository.
+	// +optional
+	PushRules *PushRules `json:"pushRules,omitempty"`
+
 	// Enable Delete source branch option by default for all new merge requests.
 	// +optional
 	RemoveSourceBranchAfterMerge *bool `json:"removeSourceBranchAfterMerge,omitempty"`
@@ -372,6 +400,14 @@ type ProjectParameters struct {
 	// +optional
 	SuggestionCommitMessage *string `json:"suggestionCommitMessage,omitempty"`
 
+	// The list of tags for a project; put array of tags,
+	// that should be finally assigned to a project.
+	//
+	// Deprecated: Use topics instead.
+	//
+	// +optional
+	TagList []string `json:"tagList,omitempty"`
+
 	// The list of topics for the project;
 	// +optional
 	Topics []string `json:"topics,omitempty"`
@@ -407,6 +443,49 @@ type ProjectParameters struct {
 	PermanentlyRemove *bool `json:"permanentlyRemove,omitempty"`
 }
 
+type PushRules struct {
+	// All commit author emails must match this regular expression.
+	AuthorEmailRegex *string `json:"authorEmailRegex,omitempty"`
+
+	// All branch names must match this regular expression.
+	BranchNameRegex *string `json:"branchNameRegex,omitempty"`
+
+	// Users can only push commits to this repository if the committer email is
+	// one of their own verified emails.
+	CommitCommitterCheck *bool `json:"commitCommitterCheck,omitempty"`
+
+	// Users can only push commits to this repository if the commit author name
+	// is consistent with their GitLab account name.
+	CommitCommitterNameCheck *bool `json:"commitCommitterNameCheck,omitempty"`
+
+	// No commit message is allowed to match this regular expression.
+	CommitMessageNegativeRegex *string `json:"commitMessageNegativeRegex,omitempty"`
+
+	// All commit messages must match this regular expression.
+	CommitMessageRegex *string `json:"commitMessageRegex,omitempty"`
+
+	// Deny deleting a tag.
+	DenyDeleteTag *bool `json:"denyDeleteTag,omitempty"`
+
+	// All committed filenames must not match this regular expression.
+	FileNameRegex *string `json:"fileNameRegex,omitempty"`
+
+	// Maximum file size (MB).
+	MaxFileSize *int `json:"maxFileSize,omitempty"`
+
+	// Restrict commits by author (email) to existing GitLab users.
+	MemberCheck *bool `json:"memberCheck,omitempty"`
+
+	// GitLab rejects any files that are likely to contain secrets.
+	PreventSecrets *bool `json:"preventSecrets,omitempty"`
+
+	// Reject commit when it’s not signed.
+	RejectUnsignedCommits *bool `json:"rejectUnsignedCommits,omitempty"`
+
+	// Reject commit when it’s not DCO certified.
+	RejectNonDCOCommits *bool `json:"rejectNonDcoCommits,omitempty"`
+}
+
 // ProjectNamespace represents a project namespace.
 type ProjectNamespace struct {
 	ID        int    `json:"ID"`
@@ -512,12 +591,16 @@ type ProjectObservation struct {
 	HTTPURLToRepo             string                     `json:"httpUrlToRepo,omitempty"`
 	ImportError               string                     `json:"importError,omitempty"`
 	ImportStatus              string                     `json:"importStatus,omitempty"`
+	IssuesEnabled             bool                       `json:"issuesEnabled,omitempty"`
+	JobsEnabled               bool                       `json:"jobsEnabled,omitempty"`
 	IssuesAccessLevel         AccessControlValue         `json:"issuesAccessLevel,omitempty"`
 	BuildsAccessLevel         AccessControlValue         `json:"buildsAccessLevel,omitempty"`
 	LastActivityAt            *metav1.Time               `json:"lastActivityAt,omitempty"`
 	License                   *ProjectLicense            `json:"license,omitempty"`
 	LicenseURL                string                     `json:"licenseUrl,omitempty"`
 	Links                     *Links                     `json:"links,omitempty"`
+	MarkedForDeletionAt       *metav1.Time               `json:"markedForDeletionAt,omitempty"`
+	MergeRequestsEnabled      bool                       `json:"mergeRequestsEnabled,omitempty"`
 	MarkedForDeletionOn       *metav1.Time               `json:"markedForDeletionOn,omitempty"`
 	MergeRequestsAccessLevel  AccessControlValue         `json:"mergeRequestsAccessLevel,omitempty"`
 	NameWithNamespace         string                     `json:"nameWithNamespace,omitempty"`
@@ -531,10 +614,12 @@ type ProjectObservation struct {
 	SSHURLToRepo              string                     `json:"sshUrlToRepo,omitempty"`
 	ServiceDeskAddress        string                     `json:"serviceDeskAddress,omitempty"`
 	SharedWithGroups          []SharedWithGroups         `json:"sharedWithGroups,omitempty"`
+	SnippetsEnabled           bool                       `json:"snippetsEnabled,omitempty"`
 	SnippetsAccessLevel       AccessControlValue         `json:"snippetsAccessLevel,omitempty"`
 	StarCount                 int                        `json:"starCount,omitempty"`
 	Statistics                *ProjectStatistics         `json:"statistics,omitempty"`
 	WebURL                    string                     `json:"webUrl,omitempty"`
+	WikiEnabled               bool                       `json:"wikiEnabled,omitempty"`
 	WikiAccessLevel           AccessControlValue         `json:"wikiAccessLevel,omitempty"`
 }
 
diff --git a/apis/projects/v1alpha1/zz_generated.deepcopy.go b/apis/projects/v1alpha1/zz_generated.deepcopy.go
index 1b9b4f9..de8ecff 100644
--- a/apis/projects/v1alpha1/zz_generated.deepcopy.go
+++ b/apis/projects/v1alpha1/zz_generated.deepcopy.go
@@ -1376,6 +1376,10 @@ func (in *ProjectObservation) DeepCopyInto(out *ProjectObservation) {
 		*out = new(Links)
 		**out = **in
 	}
+	if in.MarkedForDeletionAt != nil {
+		in, out := &in.MarkedForDeletionAt, &out.MarkedForDeletionAt
+		*out = (*in).DeepCopy()
+	}
 	if in.MarkedForDeletionOn != nil {
 		in, out := &in.MarkedForDeletionOn, &out.MarkedForDeletionOn
 		*out = (*in).DeepCopy()
@@ -1425,6 +1429,11 @@ func (in *ProjectParameters) DeepCopyInto(out *ProjectParameters) {
 		*out = new(bool)
 		**out = **in
 	}
+	if in.ApprovalsBeforeMerge != nil {
+		in, out := &in.ApprovalsBeforeMerge, &out.ApprovalsBeforeMerge
+		*out = new(int)
+		**out = **in
+	}
 	if in.AutoCancelPendingPipelines != nil {
 		in, out := &in.AutoCancelPendingPipelines, &out.AutoCancelPendingPipelines
 		*out = new(string)
@@ -1485,6 +1494,11 @@ func (in *ProjectParameters) DeepCopyInto(out *ProjectParameters) {
 		*out = new(ContainerExpirationPolicyAttributes)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.ContainerRegistryEnabled != nil {
+		in, out := &in.ContainerRegistryEnabled, &out.ContainerRegistryEnabled
+		*out = new(bool)
+		**out = **in
+	}
 	if in.ContainerRegistryAccessLevel != nil {
 		in, out := &in.ContainerRegistryAccessLevel, &out.ContainerRegistryAccessLevel
 		*out = new(AccessControlValue)
@@ -1645,6 +1659,11 @@ func (in *ProjectParameters) DeepCopyInto(out *ProjectParameters) {
 		*out = new(bool)
 		**out = **in
 	}
+	if in.PushRules != nil {
+		in, out := &in.PushRules, &out.PushRules
+		*out = new(PushRules)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.RemoveSourceBranchAfterMerge != nil {
 		in, out := &in.RemoveSourceBranchAfterMerge, &out.RemoveSourceBranchAfterMerge
 		*out = new(bool)
@@ -1685,6 +1704,11 @@ func (in *ProjectParameters) DeepCopyInto(out *ProjectParameters) {
 		*out = new(string)
 		**out = **in
 	}
+	if in.TagList != nil {
+		in, out := &in.TagList, &out.TagList
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
 	if in.Topics != nil {
 		in, out := &in.Topics, &out.Topics
 		*out = make([]string, len(*in))
@@ -1782,6 +1806,86 @@ func (in *ProjectStatus) DeepCopy() *ProjectStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PushRules) DeepCopyInto(out *PushRules) {
+	*out = *in
+	if in.AuthorEmailRegex != nil {
+		in, out := &in.AuthorEmailRegex, &out.AuthorEmailRegex
+		*out = new(string)
+		**out = **in
+	}
+	if in.BranchNameRegex != nil {
+		in, out := &in.BranchNameRegex, &out.BranchNameRegex
+		*out = new(string)
+		**out = **in
+	}
+	if in.CommitCommitterCheck != nil {
+		in, out := &in.CommitCommitterCheck, &out.CommitCommitterCheck
+		*out = new(bool)
+		**out = **in
+	}
+	if in.CommitCommitterNameCheck != nil {
+		in, out := &in.CommitCommitterNameCheck, &out.CommitCommitterNameCheck
+		*out = new(bool)
+		**out = **in
+	}
+	if in.CommitMessageNegativeRegex != nil {
+		in, out := &in.CommitMessageNegativeRegex, &out.CommitMessageNegativeRegex
+		*out = new(string)
+		**out = **in
+	}
+	if in.CommitMessageRegex != nil {
+		in, out := &in.CommitMessageRegex, &out.CommitMessageRegex
+		*out = new(string)
+		**out = **in
+	}
+	if in.DenyDeleteTag != nil {
+		in, out := &in.DenyDeleteTag, &out.DenyDeleteTag
+		*out = new(bool)
+		**out = **in
+	}
+	if in.FileNameRegex != nil {
+		in, out := &in.FileNameRegex, &out.FileNameRegex
+		*out = new(string)
+		**out = **in
+	}
+	if in.MaxFileSize != nil {
+		in, out := &in.MaxFileSize, &out.MaxFileSize
+		*out = new(int)
+		**out = **in
+	}
+	if in.MemberCheck != nil {
+		in, out := &in.MemberCheck, &out.MemberCheck
+		*out = new(bool)
+		**out = **in
+	}
+	if in.PreventSecrets != nil {
+		in, out := &in.PreventSecrets, &out.PreventSecrets
+		*out = new(bool)
+		**out = **in
+	}
+	if in.RejectUnsignedCommits != nil {
+		in, out := &in.RejectUnsignedCommits, &out.RejectUnsignedCommits
+		*out = new(bool)
+		**out = **in
+	}
+	if in.RejectNonDCOCommits != nil {
+		in, out := &in.RejectNonDCOCommits, &out.RejectNonDCOCommits
+		*out = new(bool)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushRules.
+func (in *PushRules) DeepCopy() *PushRules {
+	if in == nil {
+		return nil
+	}
+	out := new(PushRules)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *SharedWithGroups) DeepCopyInto(out *SharedWithGroups) {
 	*out = *in
diff --git a/package/crds/projects.gitlab.crossplane.io_projects.yaml b/package/crds/projects.gitlab.crossplane.io_projects.yaml
index 813ae79..ab23dd2 100644
--- a/package/crds/projects.gitlab.crossplane.io_projects.yaml
+++ b/package/crds/projects.gitlab.crossplane.io_projects.yaml
@@ -78,6 +78,11 @@ spec:
                     description: Set whether or not merge requests can be merged with
                       skipped jobs.
                     type: boolean
+                  approvalsBeforeMerge:
+                    description: |-
+                      How many approvers should approve merge request by default.More actions
+                      To configure approval rules, see Merge request approvals API.
+                    type: integer
                   autoCancelPendingPipelines:
                     description: Auto-cancel pending pipelines. This isn’t a boolean,
                       but enabled/disabled.
@@ -139,6 +144,12 @@ spec:
                   containerRegistryAccessLevel:
                     description: Set visibility of container registry.
                     type: string
+                  containerRegistryEnabled:
+                    description: |-
+                      Enable container registry for this project.
+
+                      Deprecated: Use ContainerRegistryAccessLevel.
+                    type: boolean
                   defaultBranch:
                     description: The default branch name. Requires initializeWithReadme
                       to be true.
@@ -324,6 +335,62 @@ spec:
                   publicBuilds:
                     description: If true, jobs can be viewed by non-project members.
                     type: boolean
+                  pushRules:
+                    description: |-
+                      PushRule specifies are pre-receive Git hooks you can enable in a
+                      user-friendly interface.
+                      Push rules give you more control over what can and can’t be pushed to
+                      your repository.
+                    properties:
+                      authorEmailRegex:
+                        description: All commit author emails must match this regular
+                          expression.
+                        type: string
+                      branchNameRegex:
+                        description: All branch names must match this regular expression.
+                        type: string
+                      commitCommitterCheck:
+                        description: |-
+                          Users can only push commits to this repository if the committer email is
+                          one of their own verified emails.
+                        type: boolean
+                      commitCommitterNameCheck:
+                        description: |-
+                          Users can only push commits to this repository if the commit author name
+                          is consistent with their GitLab account name.
+                        type: boolean
+                      commitMessageNegativeRegex:
+                        description: No commit message is allowed to match this regular
+                          expression.
+                        type: string
+                      commitMessageRegex:
+                        description: All commit messages must match this regular expression.
+                        type: string
+                      denyDeleteTag:
+                        description: Deny deleting a tag.
+                        type: boolean
+                      fileNameRegex:
+                        description: All committed filenames must not match this regular
+                          expression.
+                        type: string
+                      maxFileSize:
+                        description: Maximum file size (MB).
+                        type: integer
+                      memberCheck:
+                        description: Restrict commits by author (email) to existing
+                          GitLab users.
+                        type: boolean
+                      preventSecrets:
+                        description: GitLab rejects any files that are likely to contain
+                          secrets.
+                        type: boolean
+                      rejectNonDcoCommits:
+                        description: Reject commit when it’s not DCO certified.
+                        type: boolean
+                      rejectUnsignedCommits:
+                        description: Reject commit when it’s not signed.
+                        type: boolean
+                    type: object
                   removeSourceBranchAfterMerge:
                     description: Enable Delete source branch option by default for
                       all new merge requests.
@@ -350,6 +417,15 @@ spec:
                   suggestionCommitMessage:
                     description: The commit message used to apply merge request suggestions.
                     type: string
+                  tagList:
+                    description: |-
+                      The list of tags for a project; put array of tags,
+                      that should be finally assigned to a project.
+
+                      Deprecated: Use topics instead.
+                    items:
+                      type: string
+                    type: array
                   templateName:
                     description: |-
                       When used without useCustomTemplate, name of a built-in project template.
@@ -660,6 +736,10 @@ spec:
 
                       GitLab API docs: https://docs.gitlab.com/ce/api/projects.html
                     type: string
+                  issuesEnabled:
+                    type: boolean
+                  jobsEnabled:
+                    type: boolean
                   lastActivityAt:
                     format: date-time
                     type: string
@@ -713,6 +793,9 @@ spec:
                     - repoBranches
                     - self
                     type: object
+                  markedForDeletionAt:
+                    format: date-time
+                    type: string
                   markedForDeletionOn:
                     format: date-time
                     type: string
@@ -723,6 +806,8 @@ spec:
 
                       GitLab API docs: https://docs.gitlab.com/ce/api/projects.html
                     type: string
+                  mergeRequestsEnabled:
+                    type: boolean
                   nameWithNamespace:
                     type: string
                   namespace:
@@ -923,6 +1008,8 @@ spec:
 
                       GitLab API docs: https://docs.gitlab.com/ce/api/projects.html
                     type: string
+                  snippetsEnabled:
+                    type: boolean
                   sshUrlToRepo:
                     type: string
                   starCount:
@@ -961,6 +1048,8 @@ spec:
 
                       GitLab API docs: https://docs.gitlab.com/ce/api/projects.html
                     type: string
+                  wikiEnabled:
+                    type: boolean
                 type: object
               conditions:
                 description: Conditions of the resource.
diff --git a/pkg/clients/gitlab.go b/pkg/clients/gitlab.go
index 9de1258..a1c44a8 100644
--- a/pkg/clients/gitlab.go
+++ b/pkg/clients/gitlab.go
@@ -142,15 +142,38 @@ func UseProviderConfig(ctx context.Context, c client.Client, mg resource.Managed
 	}
 }
 
-// LateInitializeStringPtr returns `from` if `in` is nil and `from` is non-empty,
-// in other cases it returns `in`.
-func LateInitializeStringPtr(in *string, from string) *string {
-	if in == nil && from != "" {
+// LateInitialize return in if not nil or from.
+func LateInitialize[T any](in, from *T) *T {
+	if in != nil {
+		return in
+	}
+	return from
+}
+
+// LateInitializeFromValue returns in if not nil or a pointer to from.
+func LateInitializeFromValue[T any](in *T, from T) *T {
+	if in != nil {
+		return in
+	}
+	return &from
+}
+
+// LateInitializeFromValue returns from if in is nil and from is not the zero
+// value of T. Otherwise it returns in.
+func LateInitializeFromValueIfNotZero[T comparable](in *T, from T) *T {
+	var zeroValue T
+	if in == nil && from != zeroValue {
 		return &from
 	}
 	return in
 }
 
+// LateInitializeStringPtr returns `from` if `in` is nil and `from` is non-empty,
+// in other cases it returns `in`.
+func LateInitializeStringPtr(in *string, from string) *string {
+	return LateInitializeFromValueIfNotZero(in, from)
+}
+
 // LateInitializeAccessControlValue returns in if it's non-nil, otherwise returns from
 // which is the backup for the cases in is nil.
 func LateInitializeAccessControlValue(in *v1alpha1.AccessControlValue, from gitlab.AccessControlValue) *v1alpha1.AccessControlValue {
diff --git a/pkg/clients/projects/fake/fake.go b/pkg/clients/projects/fake/fake.go
index 8dc0e40..ce5c95c 100644
--- a/pkg/clients/projects/fake/fake.go
+++ b/pkg/clients/projects/fake/fake.go
@@ -71,6 +71,9 @@ type MockClient struct {
 	MockDeletePipelineScheduleVariable func(pid interface{}, schedule int, key string, options ...gitlab.RequestOptionFunc) (*gitlab.PipelineVariable, *gitlab.Response, error)
 
 	MockListUsers func(opt *gitlab.ListUsersOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.User, *gitlab.Response, error)
+
+	MockGetProjectPushRules func(pid interface{}, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectPushRules, *gitlab.Response, error)
+	MockEditProjectPushRule func(pid interface{}, opt *gitlab.EditProjectPushRuleOptions, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectPushRules, *gitlab.Response, error)
 }
 
 // GetPipelineSchedule calls the underlying MockGetPipelineSchedule method.
@@ -252,3 +255,13 @@ func (c *MockClient) RevokeProjectAccessToken(pid interface{}, id int, options .
 func (c *MockClient) ListUsers(opt *gitlab.ListUsersOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.User, *gitlab.Response, error) {
 	return c.MockListUsers(opt)
 }
+
+// GetProjectPushRules calls the underlying MockGetProjectPushRules method.
+func (c *MockClient) GetProjectPushRules(pid interface{}, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectPushRules, *gitlab.Response, error) {
+	return c.MockGetProjectPushRules(pid, options...)
+}
+
+// EditProjectPushRule calls the underlying MockEditProjectPushRule method.
+func (c *MockClient) EditProjectPushRule(pid interface{}, opt *gitlab.EditProjectPushRuleOptions, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectPushRules, *gitlab.Response, error) {
+	return c.MockEditProjectPushRule(pid, opt, options...)
+}
diff --git a/pkg/clients/projects/project.go b/pkg/clients/projects/project.go
index d18c963..75c76cd 100644
--- a/pkg/clients/projects/project.go
+++ b/pkg/clients/projects/project.go
@@ -37,6 +37,9 @@ type Client interface {
 	CreateProject(opt *gitlab.CreateProjectOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Project, *gitlab.Response, error)
 	EditProject(pid interface{}, opt *gitlab.EditProjectOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Project, *gitlab.Response, error)
 	DeleteProject(pid interface{}, opt *gitlab.DeleteProjectOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
+
+	GetProjectPushRules(pid interface{}, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectPushRules, *gitlab.Response, error)
+	EditProjectPushRule(pid interface{}, opt *gitlab.EditProjectPushRuleOptions, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectPushRules, *gitlab.Response, error)
 }
 
 // NewProjectClient returns a new Gitlab Project service
@@ -69,11 +72,16 @@ func GenerateObservation(prj *gitlab.Project) v1alpha1.ProjectObservation { //no
 		ReadmeURL:                prj.ReadmeURL,
 		NameWithNamespace:        prj.NameWithNamespace,
 		PathWithNamespace:        prj.PathWithNamespace,
+		IssuesEnabled:            prj.IssuesEnabled, //nolint:staticcheck
 		IssuesAccessLevel:        v1alpha1.AccessControlValue(prj.IssuesAccessLevel),
 		OpenIssuesCount:          prj.OpenIssuesCount,
+		MergeRequestsEnabled:     prj.MergeRequestsEnabled, //nolint:staticcheck
 		MergeRequestsAccessLevel: v1alpha1.AccessControlValue(prj.MergeRequestsAccessLevel),
+		JobsEnabled:              prj.JobsEnabled, //nolint:staticcheck
 		BuildsAccessLevel:        v1alpha1.AccessControlValue(prj.BuildsAccessLevel),
+		WikiEnabled:              prj.WikiEnabled, //nolint:staticcheck
 		WikiAccessLevel:          v1alpha1.AccessControlValue(prj.WikiAccessLevel),
+		SnippetsEnabled:          prj.SnippetsEnabled, //nolint:staticcheck
 		SnippetsAccessLevel:      v1alpha1.AccessControlValue(prj.SnippetsAccessLevel),
 		CreatorID:                prj.CreatorID,
 		ImportStatus:             prj.ImportStatus,
@@ -115,6 +123,9 @@ func GenerateObservation(prj *gitlab.Project) v1alpha1.ProjectObservation { //no
 	if prj.LastActivityAt != nil {
 		o.LastActivityAt = &metav1.Time{Time: *prj.LastActivityAt}
 	}
+	if prj.MarkedForDeletionAt != nil { //nolint:staticcheck
+		o.MarkedForDeletionAt = &metav1.Time{Time: time.Time(*prj.MarkedForDeletionAt)} //nolint:staticcheck
+	}
 	if prj.MarkedForDeletionOn != nil {
 		o.MarkedForDeletionOn = &metav1.Time{Time: time.Time(*prj.MarkedForDeletionOn)}
 	}
@@ -292,6 +303,7 @@ func GenerateCreateProjectOptions(name string, p *v1alpha1.ProjectParameters) *g
 		EmailsDisabled:                      p.EmailsDisabled,
 		ResolveOutdatedDiffDiscussions:      p.ResolveOutdatedDiffDiscussions,
 		ContainerExpirationPolicyAttributes: clients.ContainerExpirationPolicyAttributesV1alpha1ToGitlab(p.ContainerExpirationPolicyAttributes),
+		ContainerRegistryEnabled:            p.ContainerRegistryEnabled, //nolint:staticcheck
 		ContainerRegistryAccessLevel:        clients.AccessControlValueV1alpha1ToGitlab(p.ContainerRegistryAccessLevel),
 		SharedRunnersEnabled:                p.SharedRunnersEnabled,
 		Visibility:                          clients.VisibilityValueV1alpha1ToGitlab(p.Visibility),
@@ -304,6 +316,7 @@ func GenerateCreateProjectOptions(name string, p *v1alpha1.ProjectParameters) *g
 		RemoveSourceBranchAfterMerge:             p.RemoveSourceBranchAfterMerge,
 		LFSEnabled:                               p.LFSEnabled,
 		RequestAccessEnabled:                     p.RequestAccessEnabled,
+		TagList:                                  &p.TagList, //nolint:staticcheck
 		Topics:                                   &p.Topics,
 		PrintingMergeRequestLinkEnabled:          p.PrintingMergeRequestLinkEnabled,
 		BuildGitStrategy:                         p.BuildGitStrategy,
@@ -314,6 +327,7 @@ func GenerateCreateProjectOptions(name string, p *v1alpha1.ProjectParameters) *g
 		CIForwardDeploymentEnabled:               p.CIForwardDeploymentEnabled,
 		AutoDevopsEnabled:                        p.AutoDevopsEnabled,
 		AutoDevopsDeployStrategy:                 p.AutoDevopsDeployStrategy,
+		ApprovalsBeforeMerge:                     p.ApprovalsBeforeMerge,
 		ExternalAuthorizationClassificationLabel: p.ExternalAuthorizationClassificationLabel,
 		Mirror:                                   p.Mirror,
 		MirrorTriggerBuilds:                      p.MirrorTriggerBuilds,
@@ -355,6 +369,7 @@ func GenerateEditProjectOptions(name string, p *v1alpha1.ProjectParameters) *git
 		EmailsDisabled:                      p.EmailsDisabled,
 		ResolveOutdatedDiffDiscussions:      p.ResolveOutdatedDiffDiscussions,
 		ContainerExpirationPolicyAttributes: clients.ContainerExpirationPolicyAttributesV1alpha1ToGitlab(p.ContainerExpirationPolicyAttributes),
+		ContainerRegistryEnabled:            p.ContainerRegistryEnabled, //nolint:staticcheck
 		ContainerRegistryAccessLevel:        clients.AccessControlValueV1alpha1ToGitlab(p.ContainerRegistryAccessLevel),
 		SharedRunnersEnabled:                p.SharedRunnersEnabled,
 		Visibility:                          clients.VisibilityValueV1alpha1ToGitlab(p.Visibility),
@@ -367,6 +382,7 @@ func GenerateEditProjectOptions(name string, p *v1alpha1.ProjectParameters) *git
 		RemoveSourceBranchAfterMerge:             p.RemoveSourceBranchAfterMerge,
 		LFSEnabled:                               p.LFSEnabled,
 		RequestAccessEnabled:                     p.RequestAccessEnabled,
+		TagList:                                  &p.TagList, //nolint:staticcheck
 		Topics:                                   &p.Topics,
 		BuildGitStrategy:                         p.BuildGitStrategy,
 		BuildTimeout:                             p.BuildTimeout,
@@ -377,6 +393,7 @@ func GenerateEditProjectOptions(name string, p *v1alpha1.ProjectParameters) *git
 		CIDefaultGitDepth:                        p.CIDefaultGitDepth,
 		AutoDevopsEnabled:                        p.AutoDevopsEnabled,
 		AutoDevopsDeployStrategy:                 p.AutoDevopsDeployStrategy,
+		ApprovalsBeforeMerge:                     p.ApprovalsBeforeMerge,
 		ExternalAuthorizationClassificationLabel: p.ExternalAuthorizationClassificationLabel,
 		Mirror:                                   p.Mirror,
 		MirrorUserID:                             p.MirrorUserID,
@@ -392,3 +409,23 @@ func GenerateEditProjectOptions(name string, p *v1alpha1.ProjectParameters) *git
 	}
 	return o
 }
+
+func GenerateEditPushRulesOptions(p *v1alpha1.ProjectParameters) *gitlab.EditProjectPushRuleOptions {
+	o := &gitlab.EditProjectPushRuleOptions{}
+	if p.PushRules != nil {
+		o.AuthorEmailRegex = p.PushRules.AuthorEmailRegex
+		o.BranchNameRegex = p.PushRules.BranchNameRegex
+		o.CommitCommitterCheck = p.PushRules.CommitCommitterCheck
+		o.CommitCommitterNameCheck = p.PushRules.CommitCommitterNameCheck
+		o.CommitMessageNegativeRegex = p.PushRules.CommitMessageNegativeRegex
+		o.CommitMessageRegex = p.PushRules.CommitMessageRegex
+		o.DenyDeleteTag = p.PushRules.DenyDeleteTag
+		o.FileNameRegex = p.PushRules.FileNameRegex
+		o.MaxFileSize = p.PushRules.MaxFileSize
+		o.MemberCheck = p.PushRules.MemberCheck
+		o.PreventSecrets = p.PushRules.PreventSecrets
+		o.RejectNonDCOCommits = p.PushRules.RejectNonDCOCommits
+		o.RejectUnsignedCommits = p.PushRules.RejectUnsignedCommits
+	}
+	return o
+}
diff --git a/pkg/controller/projects/projects/project.go b/pkg/controller/projects/projects/project.go
index a6b8776..f5044c7 100644
--- a/pkg/controller/projects/projects/project.go
+++ b/pkg/controller/projects/projects/project.go
@@ -45,12 +45,17 @@ import (
 )
 
 const (
-	errNotProject       = "managed resource is not a Gitlab project custom resource"
-	errKubeUpdateFailed = "cannot update Gitlab project custom resource"
-	errCreateFailed     = "cannot create Gitlab project"
-	errUpdateFailed     = "cannot update Gitlab project"
-	errDeleteFailed     = "cannot delete Gitlab project"
-	errGetFailed        = "cannot retrieve Gitlab project with"
+	errNotProject              = "managed resource is not a Gitlab project custom resource"
+	errKubeUpdateFailed        = "cannot update Gitlab project custom resource"
+	errCreateFailed            = "cannot create Gitlab project"
+	errUpdateFailed            = "cannot update Gitlab project"
+	errUpdatePushRulesFailed   = "cannot update Gitlab project push rules"
+	errDeleteFailed            = "cannot delete Gitlab project"
+	errGetFailed               = "cannot retrieve Gitlab project with"
+	errGetPushRulesFailed      = "cannot retrieve Gitlab project push rules"
+	errLateInitialize          = "cannot late-initialize Gitlab project"
+	errLateInitializePushRules = "cannot late-initialize Gitlab project push rules"
+	errCheckPushRulesUpToDate  = "cannot compare project push rules"
 )
 
 // SetupProject adds a controller that reconciles Projects.
@@ -110,6 +115,11 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E
 type external struct {
 	kube   client.Client
 	client projects.Client
+
+	cache struct {
+		externalPushRules   *v1alpha1.PushRules
+		isPushRulesUpToDate bool
+	}
 }
 
 func (e *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) {
@@ -137,14 +147,21 @@ func (e *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex
 	}
 
 	current := cr.Spec.ForProvider.DeepCopy()
-	lateInitialize(&cr.Spec.ForProvider, prj)
+	if err := e.lateInitialize(ctx, cr, prj); err != nil {
+		return managed.ExternalObservation{}, errors.Wrap(err, errLateInitialize)
+	}
+
+	e.cache.isPushRulesUpToDate, err = e.isPushRulesUpToDate(ctx, cr)
+	if err != nil {
+		return managed.ExternalObservation{}, errors.Wrap(err, errCheckPushRulesUpToDate)
+	}
 
 	cr.Status.AtProvider = projects.GenerateObservation(prj)
 	cr.Status.SetConditions(xpv1.Available())
 
 	return managed.ExternalObservation{
 		ResourceExists:          true,
-		ResourceUpToDate:        isProjectUpToDate(&cr.Spec.ForProvider, prj),
+		ResourceUpToDate:        isProjectUpToDate(&cr.Spec.ForProvider, prj) && e.cache.isPushRulesUpToDate,
 		ResourceLateInitialized: !cmp.Equal(current, &cr.Spec.ForProvider),
 		ConnectionDetails:       managed.ConnectionDetails{"runnersToken": []byte(prj.RunnersToken)},
 	}, nil
@@ -179,8 +196,21 @@ func (e *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext
 		projects.GenerateEditProjectOptions(cr.Name, &cr.Spec.ForProvider),
 		gitlab.WithContext(ctx),
 	)
+	if err != nil {
+		return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateFailed)
+	}
 
-	return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateFailed)
+	if !e.cache.isPushRulesUpToDate {
+		_, _, err := e.client.EditProjectPushRule(
+			meta.GetExternalName(cr),
+			projects.GenerateEditPushRulesOptions(&cr.Spec.ForProvider),
+			gitlab.WithContext(ctx),
+		)
+		if err != nil {
+			return managed.ExternalUpdate{}, errors.Wrap(err, errUpdatePushRulesFailed)
+		}
+	}
+	return managed.ExternalUpdate{}, nil
 }
 
 func (e *external) Delete(ctx context.Context, mg resource.Managed) (managed.ExternalDelete, error) {
@@ -211,13 +241,18 @@ func (e *external) Disconnect(ctx context.Context) error {
 
 // lateInitialize fills the empty fields in the project spec with the
 // values seen in gitlab.Project.
-func lateInitialize(in *v1alpha1.ProjectParameters, project *gitlab.Project) { //nolint:gocyclo
+func (e *external) lateInitialize(ctx context.Context, cr *v1alpha1.Project, project *gitlab.Project) error { //nolint:gocyclo
+	in := &cr.Spec.ForProvider
+
 	if project == nil {
-		return
+		return nil
 	}
 	if in.AllowMergeOnSkippedPipeline == nil {
 		in.AllowMergeOnSkippedPipeline = &project.AllowMergeOnSkippedPipeline
 	}
+	if in.ApprovalsBeforeMerge == nil {
+		in.ApprovalsBeforeMerge = &project.ApprovalsBeforeMerge //nolint:staticcheck
+	}
 	if in.AutocloseReferencedIssues == nil {
 		in.AutocloseReferencedIssues = &project.AutocloseReferencedIssues
 	}
@@ -232,6 +267,9 @@ func lateInitialize(in *v1alpha1.ProjectParameters, project *gitlab.Project) { /
 	if in.CIForwardDeploymentEnabled == nil {
 		in.CIForwardDeploymentEnabled = &project.CIForwardDeploymentEnabled
 	}
+	if in.ContainerRegistryEnabled == nil { //nolint:staticcheck
+		in.ContainerRegistryEnabled = &project.ContainerRegistryEnabled //nolint:staticcheck
+	}
 	if in.ContainerRegistryAccessLevel == nil {
 		in.ContainerRegistryAccessLevel = clients.LateInitializeAccessControlValue(in.ContainerRegistryAccessLevel, project.ContainerRegistryAccessLevel)
 	}
@@ -306,12 +344,76 @@ func lateInitialize(in *v1alpha1.ProjectParameters, project *gitlab.Project) { /
 	in.SnippetsAccessLevel = clients.LateInitializeAccessControlValue(in.SnippetsAccessLevel, project.SnippetsAccessLevel)
 	in.SuggestionCommitMessage = clients.LateInitializeStringPtr(in.SuggestionCommitMessage, project.SuggestionCommitMessage)
 
+	if len(in.TagList) == 0 && len(project.TagList) > 0 { //nolint:staticcheck
+		in.TagList = project.TagList //nolint:staticcheck
+	}
 	if len(in.Topics) == 0 && len(project.Topics) > 0 {
 		in.Topics = project.Topics
 	}
 
 	in.Visibility = clients.LateInitializeVisibilityValue(in.Visibility, project.Visibility)
 	in.WikiAccessLevel = clients.LateInitializeAccessControlValue(in.WikiAccessLevel, project.WikiAccessLevel)
+
+	if err := e.lateInitializePushRules(ctx, cr); err != nil {
+		return errors.Wrap(err, errLateInitializePushRules)
+	}
+
+	return nil
+}
+
+func (e *external) lateInitializePushRules(ctx context.Context, cr *v1alpha1.Project) error {
+	pr, err := e.getProjectPushRules(ctx, cr)
+	if err != nil || pr == nil {
+		return err
+	}
+	if cr.Spec.ForProvider.PushRules == nil {
+		cr.Spec.ForProvider.PushRules = &v1alpha1.PushRules{}
+	}
+	cr.Spec.ForProvider.PushRules = &v1alpha1.PushRules{
+		AuthorEmailRegex:           clients.LateInitialize(cr.Spec.ForProvider.PushRules.AuthorEmailRegex, pr.AuthorEmailRegex),
+		BranchNameRegex:            clients.LateInitialize(cr.Spec.ForProvider.PushRules.BranchNameRegex, pr.BranchNameRegex),
+		CommitCommitterCheck:       clients.LateInitialize(cr.Spec.ForProvider.PushRules.CommitCommitterCheck, pr.CommitCommitterCheck),
+		CommitCommitterNameCheck:   clients.LateInitialize(cr.Spec.ForProvider.PushRules.CommitCommitterNameCheck, pr.CommitCommitterNameCheck),
+		CommitMessageNegativeRegex: clients.LateInitialize(cr.Spec.ForProvider.PushRules.CommitMessageNegativeRegex, pr.CommitMessageNegativeRegex),
+		CommitMessageRegex:         clients.LateInitialize(cr.Spec.ForProvider.PushRules.CommitMessageRegex, pr.CommitMessageRegex),
+		DenyDeleteTag:              clients.LateInitialize(cr.Spec.ForProvider.PushRules.DenyDeleteTag, pr.DenyDeleteTag),
+		FileNameRegex:              clients.LateInitialize(cr.Spec.ForProvider.PushRules.FileNameRegex, pr.FileNameRegex),
+		MaxFileSize:                clients.LateInitialize(cr.Spec.ForProvider.PushRules.MaxFileSize, pr.MaxFileSize),
+		MemberCheck:                clients.LateInitialize(cr.Spec.ForProvider.PushRules.MemberCheck, pr.MemberCheck),
+		PreventSecrets:             clients.LateInitialize(cr.Spec.ForProvider.PushRules.PreventSecrets, pr.PreventSecrets),
+		RejectUnsignedCommits:      clients.LateInitialize(cr.Spec.ForProvider.PushRules.RejectUnsignedCommits, pr.RejectUnsignedCommits),
+		RejectNonDCOCommits:        clients.LateInitialize(cr.Spec.ForProvider.PushRules.RejectNonDCOCommits, pr.RejectNonDCOCommits),
+	}
+	return nil
+}
+
+func (e *external) getProjectPushRules(ctx context.Context, cr *v1alpha1.Project) (*v1alpha1.PushRules, error) {
+	if e.cache.externalPushRules != nil {
+		return e.cache.externalPushRules, nil
+	}
+	res, _, err := e.client.GetProjectPushRules(
+		meta.GetExternalName(cr),
+		gitlab.WithContext(ctx),
+	)
+	if err != nil {
+		return nil, errors.Wrap(err, errGetPushRulesFailed)
+	}
+	e.cache.externalPushRules = &v1alpha1.PushRules{
+		AuthorEmailRegex:           &res.AuthorEmailRegex,
+		BranchNameRegex:            &res.BranchNameRegex,
+		CommitCommitterCheck:       &res.CommitCommitterCheck,
+		CommitCommitterNameCheck:   &res.CommitCommitterNameCheck,
+		CommitMessageNegativeRegex: &res.CommitMessageNegativeRegex,
+		CommitMessageRegex:         &res.CommitMessageRegex,
+		DenyDeleteTag:              &res.DenyDeleteTag,
+		FileNameRegex:              &res.FileNameRegex,
+		MaxFileSize:                &res.MaxFileSize,
+		MemberCheck:                &res.MemberCheck,
+		PreventSecrets:             &res.PreventSecrets,
+		RejectUnsignedCommits:      &res.RejectUnsignedCommits,
+		RejectNonDCOCommits:        &res.RejectNonDCOCommits,
+	}
+	return e.cache.externalPushRules, nil
 }
 
 // isProjectUpToDate checks whether there is a change in any of the modifiable fields.
@@ -322,6 +424,9 @@ func isProjectUpToDate(p *v1alpha1.ProjectParameters, g *gitlab.Project) bool {
 	if !clients.IsBoolEqualToBoolPtr(p.AllowMergeOnSkippedPipeline, g.AllowMergeOnSkippedPipeline) {
 		return false
 	}
+	if !clients.IsIntEqualToIntPtr(p.ApprovalsBeforeMerge, g.ApprovalsBeforeMerge) { //nolint:staticcheck
+		return false
+	}
 	if !clients.IsBoolEqualToBoolPtr(p.AutocloseReferencedIssues, g.AutocloseReferencedIssues) {
 		return false
 	}
@@ -340,6 +445,9 @@ func isProjectUpToDate(p *v1alpha1.ProjectParameters, g *gitlab.Project) bool {
 	if !clients.IsBoolEqualToBoolPtr(p.CIForwardDeploymentEnabled, g.CIForwardDeploymentEnabled) {
 		return false
 	}
+	if !clients.IsBoolEqualToBoolPtr(p.ContainerRegistryEnabled, g.ContainerRegistryEnabled) { //nolint:staticcheck
+		return false
+	}
 	if p.ContainerRegistryAccessLevel != nil && !cmp.Equal(string(*p.ContainerRegistryAccessLevel), string(g.ContainerRegistryAccessLevel)) {
 		return false
 	}
@@ -430,6 +538,9 @@ func isProjectUpToDate(p *v1alpha1.ProjectParameters, g *gitlab.Project) bool {
 	if !cmp.Equal(p.SuggestionCommitMessage, clients.StringToPtr(g.SuggestionCommitMessage)) {
 		return false
 	}
+	if !cmp.Equal(p.TagList, g.TagList, cmpopts.EquateEmpty()) { //nolint:staticcheck
+		return false
+	}
 	if !cmp.Equal(p.Topics, g.Topics, cmpopts.EquateEmpty()) {
 		return false
 	}
@@ -441,3 +552,11 @@ func isProjectUpToDate(p *v1alpha1.ProjectParameters, g *gitlab.Project) bool {
 	}
 	return true
 }
+
+func (e *external) isPushRulesUpToDate(ctx context.Context, cr *v1alpha1.Project) (bool, error) {
+	current, err := e.getProjectPushRules(ctx, cr)
+	if err != nil {
+		return false, err
+	}
+	return cmp.Equal(cr.Spec.ForProvider.PushRules, current), nil
+}


Test patch:
diff --git a/pkg/clients/projects/project_test.go b/pkg/clients/projects/project_test.go
index 43d4ef0..1a109a8 100644
--- a/pkg/clients/projects/project_test.go
+++ b/pkg/clients/projects/project_test.go
@@ -23,6 +23,7 @@ import (
 	"github.com/google/go-cmp/cmp"
 	gitlab "gitlab.com/gitlab-org/api/client-go"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/ptr"
 
 	"github.com/crossplane-contrib/provider-gitlab/apis/projects/v1alpha1"
 	"github.com/crossplane-contrib/provider-gitlab/pkg/clients"
@@ -522,6 +523,7 @@ func TestGenerateCreateProjectOptions(t *testing.T) {
 				RemoveSourceBranchAfterMerge:             &removeSourceBranchAfterMerge,
 				LFSEnabled:                               &lfsEnabled,
 				RequestAccessEnabled:                     &requestAccessEnabled,
+				TagList:                                  ptr.To[[]string](nil),
 				Topics:                                   &topics,
 				PrintingMergeRequestLinkEnabled:          &printingMergeRequestLinkEnabled,
 				BuildGitStrategy:                         &buildGitStategy,
@@ -566,6 +568,7 @@ func TestGenerateCreateProjectOptions(t *testing.T) {
 				IssuesAccessLevel:              clients.AccessControlValueStringToGitlab(issuesAccessLevel),
 				ResolveOutdatedDiffDiscussions: &resolveOutdatedDiffDiscussions,
 				MergeMethod:                    clients.MergeMethodStringToGitlab(mergeMethod),
+				TagList:                        ptr.To[[]string](nil),
 				Topics:                         &topics,
 				BuildTimeout:                   &buildTimeout,
 			},
@@ -581,8 +584,9 @@ func TestGenerateCreateProjectOptions(t *testing.T) {
 				},
 			},
 			want: &gitlab.CreateProjectOptions{
-				Name:   &overrideName,
-				Topics: &topics,
+				Name:    &overrideName,
+				TagList: ptr.To[[]string](nil),
+				Topics:  &topics,
 			},
 		},
 	}
@@ -690,6 +694,7 @@ func TestGenerateEditProjectOptions(t *testing.T) {
 				RemoveSourceBranchAfterMerge:             &removeSourceBranchAfterMerge,
 				LFSEnabled:                               &lfsEnabled,
 				RequestAccessEnabled:                     &requestAccessEnabled,
+				TagList:                                  ptr.To[[]string](nil),
 				Topics:                                   &topics,
 				BuildGitStrategy:                         &buildGitStategy,
 				BuildTimeout:                             &buildTimeout,
@@ -732,6 +737,7 @@ func TestGenerateEditProjectOptions(t *testing.T) {
 				IssuesAccessLevel:              clients.AccessControlValueStringToGitlab(issuesAccessLevel),
 				ResolveOutdatedDiffDiscussions: &resolveOutdatedDiffDiscussions,
 				MergeMethod:                    clients.MergeMethodStringToGitlab(mergeMethod),
+				TagList:                        ptr.To[[]string](nil),
 				Topics:                         &topics,
 				BuildTimeout:                   &buildTimeout,
 			},
@@ -747,8 +753,9 @@ func TestGenerateEditProjectOptions(t *testing.T) {
 				},
 			},
 			want: &gitlab.EditProjectOptions{
-				Name:   &name,
-				Topics: &topics,
+				Name:    &name,
+				TagList: ptr.To[[]string](nil),
+				Topics:  &topics,
 			},
 		},
 	}
diff --git a/pkg/controller/projects/projects/project_test.go b/pkg/controller/projects/projects/project_test.go
index b04e443..240bc43 100644
--- a/pkg/controller/projects/projects/project_test.go
+++ b/pkg/controller/projects/projects/project_test.go
@@ -31,6 +31,7 @@ import (
 	"github.com/google/go-cmp/cmp"
 	"github.com/pkg/errors"
 	gitlab "gitlab.com/gitlab-org/api/client-go"
+	"k8s.io/utils/ptr"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/crossplane-contrib/provider-gitlab/apis/projects/v1alpha1"
@@ -75,6 +76,10 @@ func withSpec(s v1alpha1.ProjectParameters) projectModifier {
 	return func(r *v1alpha1.Project) { r.Spec.ForProvider = s }
 }
 
+func withProjectPushRules(pr *v1alpha1.PushRules) projectModifier {
+	return func(r *v1alpha1.Project) { r.Spec.ForProvider.PushRules = pr }
+}
+
 func withClientDefaultValues() projectModifier {
 	return func(p *v1alpha1.Project) {
 		f := false
@@ -85,6 +90,7 @@ func withClientDefaultValues() projectModifier {
 			NamespaceID:                               &i,
 			EmailsDisabled:                            &f,
 			ResolveOutdatedDiffDiscussions:            &f,
+			ContainerRegistryEnabled:                  &f,
 			SharedRunnersEnabled:                      &f,
 			PublicBuilds:                              &f,
 			OnlyAllowMergeIfPipelineSucceeds:          &f,
@@ -96,6 +102,7 @@ func withClientDefaultValues() projectModifier {
 			BuildTimeout:                              &i,
 			CIDefaultGitDepth:                         &i,
 			AutoDevopsEnabled:                         &f,
+			ApprovalsBeforeMerge:                      &i,
 			Mirror:                                    &f,
 			MirrorUserID:                              &i,
 			MirrorTriggerBuilds:                       &f,
@@ -258,16 +265,49 @@ func TestObserve(t *testing.T) {
 					MockGetProject: func(pid interface{}, opt *gitlab.GetProjectOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Project, *gitlab.Response, error) {
 						return &gitlab.Project{Name: "example-project"}, &gitlab.Response{}, nil
 					},
+					MockGetProjectPushRules: func(pid interface{}, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectPushRules, *gitlab.Response, error) {
+						return &gitlab.ProjectPushRules{}, nil, nil
+					},
 				},
 				cr: project(
 					withClientDefaultValues(),
 					withExternalName(extName),
+					withProjectPushRules(&v1alpha1.PushRules{
+						AuthorEmailRegex:           ptr.To(""),
+						BranchNameRegex:            ptr.To(""),
+						CommitCommitterCheck:       ptr.To(false),
+						CommitCommitterNameCheck:   ptr.To(false),
+						CommitMessageNegativeRegex: ptr.To(""),
+						CommitMessageRegex:         ptr.To(""),
+						DenyDeleteTag:              ptr.To(false),
+						FileNameRegex:              ptr.To(""),
+						MaxFileSize:                ptr.To(0),
+						MemberCheck:                ptr.To(false),
+						PreventSecrets:             ptr.To(false),
+						RejectUnsignedCommits:      ptr.To(false),
+						RejectNonDCOCommits:        ptr.To(false),
+					}),
 				),
 			},
 			want: want{
 				cr: project(
 					withClientDefaultValues(),
 					withExternalName(extName),
+					withProjectPushRules(&v1alpha1.PushRules{
+						AuthorEmailRegex:           ptr.To(""),
+						BranchNameRegex:            ptr.To(""),
+						CommitCommitterCheck:       ptr.To(false),
+						CommitCommitterNameCheck:   ptr.To(false),
+						CommitMessageNegativeRegex: ptr.To(""),
+						CommitMessageRegex:         ptr.To(""),
+						DenyDeleteTag:              ptr.To(false),
+						FileNameRegex:              ptr.To(""),
+						MaxFileSize:                ptr.To(0),
+						MemberCheck:                ptr.To(false),
+						PreventSecrets:             ptr.To(false),
+						RejectUnsignedCommits:      ptr.To(false),
+						RejectNonDCOCommits:        ptr.To(false),
+					}),
 					withConditions(xpv1.Available()),
 				),
 				result: managed.ExternalObservation{
@@ -287,6 +327,9 @@ func TestObserve(t *testing.T) {
 					MockGetProject: func(pid interface{}, opt *gitlab.GetProjectOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Project, *gitlab.Response, error) {
 						return &gitlab.Project{Path: path, RunnersToken: "token"}, &gitlab.Response{}, nil
 					},
+					MockGetProjectPushRules: func(pid interface{}, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectPushRules, *gitlab.Response, error) {
+						return &gitlab.ProjectPushRules{}, nil, nil
+					},
 				},
 				cr: project(
 					withClientDefaultValues(),
@@ -299,6 +342,21 @@ func TestObserve(t *testing.T) {
 					withConditions(xpv1.Available()),
 					withPath(&path),
 					withExternalName(extName),
+					withProjectPushRules(&v1alpha1.PushRules{
+						AuthorEmailRegex:           ptr.To(""),
+						BranchNameRegex:            ptr.To(""),
+						CommitCommitterCheck:       ptr.To(false),
+						CommitCommitterNameCheck:   ptr.To(false),
+						CommitMessageNegativeRegex: ptr.To(""),
+						CommitMessageRegex:         ptr.To(""),
+						DenyDeleteTag:              ptr.To(false),
+						FileNameRegex:              ptr.To(""),
+						MaxFileSize:                ptr.To(0),
+						MemberCheck:                ptr.To(false),
+						PreventSecrets:             ptr.To(false),
+						RejectUnsignedCommits:      ptr.To(false),
+						RejectNonDCOCommits:        ptr.To(false),
+					}),
 					withStatus(v1alpha1.ProjectObservation{}),
 				),
 				result: managed.ExternalObservation{
@@ -318,10 +376,29 @@ func TestObserve(t *testing.T) {
 					MockGetProject: func(pid interface{}, opt *gitlab.GetProjectOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Project, *gitlab.Response, error) {
 						return &gitlab.Project{MirrorUserID: 0}, &gitlab.Response{}, nil
 					},
+					MockGetProjectPushRules: func(pid interface{}, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectPushRules, *gitlab.Response, error) {
+						return &gitlab.ProjectPushRules{}, nil, nil
+					},
 				},
 				cr: project(
 					withClientDefaultValues(),
 					withMirrorUserIDNil(),
+					withProjectPushRules(&v1alpha1.PushRules{
+						AuthorEmailRegex:           ptr.To(""),
+						BranchNameRegex:            ptr.To(""),
+						CommitCommitterCheck:       ptr.To(false),
+						CommitCommitterNameCheck:   ptr.To(false),
+						CommitMessageNegativeRegex: ptr.To(""),
+						CommitMessageRegex:         ptr.To(""),
+						DenyDeleteTag:              ptr.To(false),
+						FileNameRegex:              ptr.To(""),
+						MaxFileSize:                ptr.To(0),
+						MemberCheck:                ptr.To(false),
+						PreventSecrets:             ptr.To(false),
+						RejectUnsignedCommits:      ptr.To(false),
+						RejectNonDCOCommits:        ptr.To(false),
+					},
+					),
 					withExternalName(extName),
 				),
 			},
@@ -330,6 +407,21 @@ func TestObserve(t *testing.T) {
 					withClientDefaultValues(),
 					withMirrorUserIDNil(),
 					withExternalName(extName),
+					withProjectPushRules(&v1alpha1.PushRules{
+						AuthorEmailRegex:           ptr.To(""),
+						BranchNameRegex:            ptr.To(""),
+						CommitCommitterCheck:       ptr.To(false),
+						CommitCommitterNameCheck:   ptr.To(false),
+						CommitMessageNegativeRegex: ptr.To(""),
+						CommitMessageRegex:         ptr.To(""),
+						DenyDeleteTag:              ptr.To(false),
+						FileNameRegex:              ptr.To(""),
+						MaxFileSize:                ptr.To(0),
+						MemberCheck:                ptr.To(false),
+						PreventSecrets:             ptr.To(false),
+						RejectUnsignedCommits:      ptr.To(false),
+						RejectNonDCOCommits:        ptr.To(false),
+					}),
 					withConditions(xpv1.Available()),
 				),
 				result: managed.ExternalObservation{
@@ -356,6 +448,7 @@ func TestObserve(t *testing.T) {
 		"SnippetsAccessLevel":                       gitlab.PublicAccessControl,
 		"PagesAccessLevel":                          gitlab.PublicAccessControl,
 		"ResolveOutdatedDiffDiscussions":            true,
+		"ContainerRegistryEnabled":                  true,
 		"ContainerRegistryAccessLevel":              gitlab.EnabledAccessControl,
 		"SharedRunnersEnabled":                      true,
 		"Visibility":                                gitlab.PrivateVisibility,
@@ -366,6 +459,7 @@ func TestObserve(t *testing.T) {
 		"RemoveSourceBranchAfterMerge":              true,
 		"LFSEnabled":                                true,
 		"RequestAccessEnabled":                      true,
+		"TagList":                                   []string{"tag-1", "tag-2"},
 		"Topics":                                    []string{"tag-1", "tag-2"},
 		"CIConfigPath":                              "CI configPath",
 		"CIDefaultGitDepth":                         1,
@@ -384,6 +478,7 @@ func TestObserve(t *testing.T) {
 	f := false
 	i := 0
 	al := v1alpha1.PublicAccessControl
+	tags := []string{"tag-1 new", "tag-2 new"}
 	topics := []string{"tag-1 new", "tag-2 new"}
 	mergeMethod := v1alpha1.FastForwardMerge
 	s := "default string"
@@ -397,12 +492,14 @@ func TestObserve(t *testing.T) {
 		IssuesAccessLevel:                &al,
 		RepositoryAccessLevel:            &al,
 		MergeRequestsAccessLevel:         &al,
+		ApprovalsBeforeMerge:             ptr.To(0),
 		ForkingAccessLevel:               &al,
 		BuildsAccessLevel:                &al,
 		WikiAccessLevel:                  &al,
 		SnippetsAccessLevel:              &al,
 		PagesAccessLevel:                 &al,
 		ResolveOutdatedDiffDiscussions:   &f,
+		ContainerRegistryEnabled:         &f,
 		ContainerRegistryAccessLevel:     &al,
 		SharedRunnersEnabled:             &f,
 		Visibility:                       &visibility,
@@ -413,6 +510,7 @@ func TestObserve(t *testing.T) {
 		RemoveSourceBranchAfterMerge:     &f,
 		LFSEnabled:                       &f,
 		RequestAccessEnabled:             &f,
+		TagList:                          tags,
 		Topics:                           topics,
 		CIConfigPath:                     &s,
 		CIDefaultGitDepth:                &i,
@@ -426,6 +524,21 @@ func TestObserve(t *testing.T) {
 		AutocloseReferencedIssues:        &f,
 		AllowMergeOnSkippedPipeline:      &f,
 		CIForwardDeploymentEnabled:       &f,
+		PushRules: &v1alpha1.PushRules{
+			AuthorEmailRegex:           ptr.To(""),
+			BranchNameRegex:            ptr.To(""),
+			CommitCommitterCheck:       ptr.To(false),
+			CommitCommitterNameCheck:   ptr.To(false),
+			CommitMessageNegativeRegex: ptr.To(""),
+			CommitMessageRegex:         ptr.To(""),
+			DenyDeleteTag:              ptr.To(true),
+			FileNameRegex:              ptr.To(""),
+			MaxFileSize:                ptr.To(0),
+			MemberCheck:                ptr.To(false),
+			PreventSecrets:             ptr.To(false),
+			RejectUnsignedCommits:      ptr.To(false),
+			RejectNonDCOCommits:        ptr.To(false),
+		},
 	}
 
 	for name, value := range isProjectUpToDateCases {
@@ -436,6 +549,21 @@ func TestObserve(t *testing.T) {
 		wantProjectModifier := []projectModifier{
 			withSpec(projectParameters),
 			withExternalName("0"),
+			withProjectPushRules(&v1alpha1.PushRules{
+				AuthorEmailRegex:           ptr.To(""),
+				BranchNameRegex:            ptr.To(""),
+				CommitCommitterCheck:       ptr.To(false),
+				CommitCommitterNameCheck:   ptr.To(false),
+				CommitMessageNegativeRegex: ptr.To(""),
+				CommitMessageRegex:         ptr.To(""),
+				DenyDeleteTag:              ptr.To(true),
+				FileNameRegex:              ptr.To(""),
+				MaxFileSize:                ptr.To(0),
+				MemberCheck:                ptr.To(false),
+				PreventSecrets:             ptr.To(false),
+				RejectUnsignedCommits:      ptr.To(false),
+				RejectNonDCOCommits:        ptr.To(false),
+			}),
 			withConditions(xpv1.Available()),
 			withStatus(v1alpha1.ProjectObservation{
 				IssuesAccessLevel:        al,
@@ -469,9 +597,11 @@ func TestObserve(t *testing.T) {
 			RemoveSourceBranchAfterMerge:     f,
 			LFSEnabled:                       f,
 			RequestAccessEnabled:             f,
+			TagList:                          tags,
 			Topics:                           topics,
 			CIConfigPath:                     s,
 			CIDefaultGitDepth:                i,
+			ApprovalsBeforeMerge:             i,
 			Mirror:                           f,
 			MirrorUserID:                     i,
 			MirrorTriggerBuilds:              f,
@@ -498,6 +628,11 @@ func TestObserve(t *testing.T) {
 					MockGetProject: func(pid interface{}, opt *gitlab.GetProjectOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Project, *gitlab.Response, error) {
 						return gitlabProject, &gitlab.Response{}, nil
 					},
+					MockGetProjectPushRules: func(pid interface{}, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectPushRules, *gitlab.Response, error) {
+						return &gitlab.ProjectPushRules{
+							DenyDeleteTag: true,
+						}, nil, nil
+					},
 				},
 				cr: project(argsProjectModifier...),
 			},
@@ -628,6 +763,9 @@ func TestUpdate(t *testing.T) {
 					MockEditProject: func(pid interface{}, opt *gitlab.EditProjectOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Project, *gitlab.Response, error) {
 						return &gitlab.Project{}, &gitlab.Response{}, nil
 					},
+					MockEditProjectPushRule: func(pid interface{}, opt *gitlab.EditProjectPushRuleOptions, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectPushRules, *gitlab.Response, error) {
+						return &gitlab.ProjectPushRules{}, &gitlab.Response{}, nil
+					},
 				},
 				cr: project(withStatus(v1alpha1.ProjectObservation{ID: 1234})),
 			},
@@ -641,6 +779,9 @@ func TestUpdate(t *testing.T) {
 					MockEditProject: func(pid interface{}, opt *gitlab.EditProjectOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Project, *gitlab.Response, error) {
 						return &gitlab.Project{}, &gitlab.Response{}, errBoom
 					},
+					MockEditProjectPushRule: func(pid interface{}, opt *gitlab.EditProjectPushRuleOptions, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectPushRules, *gitlab.Response, error) {
+						return &gitlab.ProjectPushRules{}, &gitlab.Response{}, nil
+					},
 				},
 				cr: project(withStatus(v1alpha1.ProjectObservation{ID: 1234})),
 			},