Repository: rancher/fleet
Pull request #2226 (2024-03-14 15:26:54)

Adds option to disable polling for Git repos
Adds the `disableGitPolling` option in order to disable polling for git repos. 

It also ports gitrepos polling to use the `quartz` library to be inline with `imagescan`, that is already using it.

Changes the unit tests to be implemented using the Ginkgo testing framework.

**NOTE**
Bumps `quartz` library to `0.11.2`, which fixes https://github.com/reugn/go-quartz/issues/119

Refers to: https://github.com/rancher/fleet/issues/1572


0xavi0: @bigkevmcd I bumped `quartz` to 0.11.2, which includes the fix for the concurrency issue and that package for doing retries is no longer needed
bigkevmcd: @0xavi0 Awesome :-)

Patch:
diff --git a/.github/scripts/build-fleet-images.sh b/.github/scripts/build-fleet-images.sh
index e809077f..2dd7a2d5 100755
--- a/.github/scripts/build-fleet-images.sh
+++ b/.github/scripts/build-fleet-images.sh
@@ -2,4 +2,3 @@ export GOARCH="${GOARCH:-amd64}"
 
 docker build -f package/Dockerfile -t rancher/fleet:dev --build-arg="ARCH=$GOARCH"  .
 docker build -f package/Dockerfile.agent -t rancher/fleet-agent:dev --build-arg="ARCH=$GOARCH" .
-docker build -f package/Dockerfile.gitjob -t rancher/gitjob:dev --build-arg="ARCH=$GOARCH" .
diff --git a/.github/scripts/deploy-fleet.sh b/.github/scripts/deploy-fleet.sh
index 68ba5415..cb8f81b7 100755
--- a/.github/scripts/deploy-fleet.sh
+++ b/.github/scripts/deploy-fleet.sh
@@ -15,15 +15,11 @@ if [ $# -ge 3 ] && [ -n "$1" ] && [ -n "$2" ] && [ -n "$3" ]; then
   fleetTag="${1#*:}"
   agentRepo="${2%:*}"
   agentTag="${2#*:}"
-  gitjobRepo="${3%:*}"
-  gitjobTag="${3#*:}"
 else
   fleetRepo="rancher/fleet"
   fleetTag="dev"
   agentRepo="rancher/fleet-agent"
   agentTag="dev"
-  gitjobRepo="rancher/gitjob"
-  gitjobTag="dev"
 fi
 
 eventually helm upgrade --install fleet-crd charts/fleet-crd \
@@ -39,8 +35,6 @@ eventually helm upgrade --install fleet charts/fleet \
   --set agentImage.repository="$agentRepo" \
   --set agentImage.tag="$agentTag" \
   --set agentImage.imagePullPolicy=IfNotPresent \
-  --set gitjob.repository="$gitjobRepo" \
-  --set gitjob.tag="$gitjobTag"
 
 # wait for controller and agent rollout
 kubectl -n cattle-fleet-system rollout status deploy/fleet-controller
diff --git a/.github/workflows/aks.yml b/.github/workflows/aks.yml
index 12e90076..6d2776a7 100644
--- a/.github/workflows/aks.yml
+++ b/.github/workflows/aks.yml
@@ -121,29 +121,12 @@ jobs:
           push: true
           tags: ${{ steps.meta-fleet-agent.outputs.tags }}
           labels: ${{ steps.meta-fleet-agent.outputs.labels }}
-      -
-        id: meta-gitjob
-        uses: docker/metadata-action@v5
-        with:
-          images: |
-            ttl.sh/rancher/gitjob-${{ steps.uuid.outputs.uuid }}
-          tags: type=raw,value=1h
-      -
-        uses: docker/build-push-action@v5
-        with:
-          context: .
-          file: package/Dockerfile.gitjob
-          build-args: |
-            ARCH=${{ env.GOARCH }}
-          push: true
-          tags: ${{ steps.meta-gitjob.outputs.tags }}
-          labels: ${{ steps.meta-gitjob.outputs.labels }}
       -
         name: Deploy Fleet
         run: |
           export KUBECONFIG="$GITHUB_WORKSPACE/kubeconfig-fleet-ci"
           echo "${{ steps.meta-fleet.outputs.tags }} ${{ steps.meta-fleet-agent.outputs.tags }}"
-          ./.github/scripts/deploy-fleet.sh ${{ steps.meta-fleet.outputs.tags }} ${{ steps.meta-fleet-agent.outputs.tags }} ${{ steps.meta-gitjob.outputs.tags }}
+          ./.github/scripts/deploy-fleet.sh ${{ steps.meta-fleet.outputs.tags }} ${{ steps.meta-fleet-agent.outputs.tags }}
       -
         name: Fleet E2E Tests
         env:
diff --git a/.github/workflows/fleet-upgrade.yml b/.github/workflows/fleet-upgrade.yml
index 3f7fa6ed..0cfddc4a 100644
--- a/.github/workflows/fleet-upgrade.yml
+++ b/.github/workflows/fleet-upgrade.yml
@@ -74,7 +74,7 @@ jobs:
       -
         name: Import Images Into k3d
         run: |
-          ./.github/scripts/k3d-import-retry.sh rancher/fleet:dev rancher/fleet-agent:dev rancher/gitjob:dev -c upstream
+          ./.github/scripts/k3d-import-retry.sh rancher/fleet:dev rancher/fleet-agent:dev -c upstream
       -
         name: Verify Example Workload
         run: |
diff --git a/.github/workflows/gke.yml b/.github/workflows/gke.yml
index ceb32525..5865c5e4 100644
--- a/.github/workflows/gke.yml
+++ b/.github/workflows/gke.yml
@@ -126,28 +126,11 @@ jobs:
           push: true
           tags: ${{ steps.meta-fleet-agent.outputs.tags }}
           labels: ${{ steps.meta-fleet-agent.outputs.labels }}
-      -
-        id: meta-gitjob
-        uses: docker/metadata-action@v5
-        with:
-          images: |
-            ttl.sh/rancher/gitjob-${{ steps.uuid.outputs.uuid }}
-          tags: type=raw,value=1h
-      -
-        uses: docker/build-push-action@v5
-        with:
-          context: .
-          file: package/Dockerfile.gitjob
-          build-args: |
-            ARCH=${{ env.GOARCH }}
-          push: true
-          tags: ${{ steps.meta-gitjob.outputs.tags }}
-          labels: ${{ steps.meta-gitjob.outputs.labels }}
       -
         name: Deploy Fleet
         run: |
           echo "${{ steps.meta-fleet.outputs.tags }} ${{ steps.meta-fleet-agent.outputs.tags }}"
-          ./.github/scripts/deploy-fleet.sh ${{ steps.meta-fleet.outputs.tags }} ${{ steps.meta-fleet-agent.outputs.tags }} ${{ steps.meta-gitjob.outputs.tags }}
+          ./.github/scripts/deploy-fleet.sh ${{ steps.meta-fleet.outputs.tags }} ${{ steps.meta-fleet-agent.outputs.tags }}
       -
         name: Fleet E2E Tests
         env:
diff --git a/.github/workflows/nightly-ci.yml b/.github/workflows/nightly-ci.yml
index 41dcd461..5c80e3aa 100644
--- a/.github/workflows/nightly-ci.yml
+++ b/.github/workflows/nightly-ci.yml
@@ -72,7 +72,7 @@ jobs:
       -
         name: Import Images Into k3d
         run: |
-          ./.github/scripts/k3d-import-retry.sh rancher/fleet:dev rancher/fleet-agent:dev rancher/gitjob:dev nginx-git:test
+          ./.github/scripts/k3d-import-retry.sh rancher/fleet:dev rancher/fleet-agent:dev nginx-git:test
       -
         name: Set Up Tmate Debug Session
         if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.enable_tmate == 'true' }}
diff --git a/.github/workflows/release-fleet.yml b/.github/workflows/release-fleet.yml
index 77dde145..f0a59b2d 100644
--- a/.github/workflows/release-fleet.yml
+++ b/.github/workflows/release-fleet.yml
@@ -88,22 +88,25 @@ jobs:
       - name: Upload charts to release
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          repo: "ghcr.io/rancher/fleet"
+          repo: "ghcr.io/rancher"
           tag: "${GITHUB_REF_NAME}"
         run: |
+          # Replace rancher/fleet, rancher/fleet-agent and rancher/gitjob image names, but not eg. rancher/kubectl
           sed -i \
-              -e "s@repository:.*@repository: $repo@" \
-              -e "s/tag:.*/tag: ${VERSION}/" \
+              -e "s@repository: rancher/\(fleet.*\|gitjob\).*@repository: $repo/\\1@" \
+              -e "s/tag:.*/tag: $tag/" \
               charts/fleet/values.yaml
 
           sed -i \
-              -e "s@repository:.*@repository: $repo@" \
-              -e "s/tag: dev/tag: ${VERSION}/" \
+              -e "s@repository: rancher/\(fleet.*\|gitjob\).*@repository: $repo/\\1@" \
+              -e "s/tag: dev/tag: $tag/" \
               charts/fleet-agent/values.yaml
 
-          helm package --version="$VERSION" --app-version="$VERSION" -d ./dist ./charts/fleet
-          helm package --version="$VERSION" --app-version="$VERSION" -d ./dist ./charts/fleet-crd
-          helm package --version="$VERSION" --app-version="$VERSION" -d ./dist ./charts/fleet-agent
+          version=${tag#v}
+
+          helm package --version="$version" --app-version="$version" -d ./dist ./charts/fleet
+          helm package --version="$version" --app-version="$version" -d ./dist ./charts/fleet-crd
+          helm package --version="$version" --app-version="$version" -d ./dist ./charts/fleet-agent
 
           for f in $(find dist/artifacts/ -name '*.tgz'); do
             gh release upload $tag $f
diff --git a/.goreleaser.yml b/.goreleaser.yml
index a3e1e388..d7fbdd72 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -208,47 +208,6 @@ dockers:
     - "--build-arg=BUILD_ENV=goreleaser"
     - "--platform=linux/arm64/v8"
     extra_files: [ "package/log.sh" ]
-  # fleet-gitjob images
-  -
-    use: buildx
-    goos: linux
-    goarch: amd64
-    ids:
-    - fleet-gitcloner
-    - fleet-gitjob
-    image_templates:
-    - "ghcr.io/rancher/fleet-gitjob:{{ .Tag }}-linux-amd64"
-    dockerfile: package/Dockerfile.gitjob
-    build_flag_templates:
-    - "--pull"
-    - "--label=org.opencontainers.image.created={{.Date}}"
-    - "--label=org.opencontainers.image.title={{.ProjectName}}"
-    - "--label=org.opencontainers.image.revision={{.FullCommit}}"
-    - "--label=org.opencontainers.image.version={{.Version}}"
-    - "--label=org.opencontainers.image.source=https://github.com/rancher/fleet"
-    - "--build-arg=ARCH=amd64"
-    - "--build-arg=BUILD_ENV=goreleaser"
-    - "--platform=linux/amd64"
-  -
-    use: buildx
-    goos: linux
-    goarch: arm64
-    ids:
-    - fleet-gitcloner
-    - fleet-gitjob
-    image_templates:
-    - "ghcr.io/rancher/fleet-gitjob:{{ .Tag }}-linux-arm64"
-    dockerfile: package/Dockerfile.gitjob
-    build_flag_templates:
-    - "--pull"
-    - "--label=org.opencontainers.image.created={{.Date}}"
-    - "--label=org.opencontainers.image.title={{.ProjectName}}"
-    - "--label=org.opencontainers.image.revision={{.FullCommit}}"
-    - "--label=org.opencontainers.image.version={{.Version}}"
-    - "--label=org.opencontainers.image.source=https://github.com/rancher/fleet"
-    - "--build-arg=ARCH=arm64"
-    - "--build-arg=BUILD_ENV=goreleaser"
-    - "--platform=linux/arm64/v8"
 
 docker_manifests:
   # https://goreleaser.com/customization/docker_manifest/
@@ -261,8 +220,3 @@ docker_manifests:
     image_templates:
     - "ghcr.io/rancher/fleet-agent:{{ .Tag }}-linux-amd64"
     - "ghcr.io/rancher/fleet-agent:{{ .Tag }}-linux-arm64"
-
-  - name_template: "ghcr.io/rancher/fleet-gitjob:{{ .Tag }}"
-    image_templates:
-    - "ghcr.io/rancher/fleet-gitjob:{{ .Tag }}-linux-amd64"
-    - "ghcr.io/rancher/fleet-gitjob:{{ .Tag }}-linux-arm64"
diff --git a/charts/fleet-crd/templates/crds.yaml b/charts/fleet-crd/templates/crds.yaml
index 11448dba..0fdeb858 100644
--- a/charts/fleet-crd/templates/crds.yaml
+++ b/charts/fleet-crd/templates/crds.yaml
@@ -5737,6 +5737,10 @@ spec:
                         in the helm history.
                       type: boolean
                   type: object
+                disablePolling:
+                  description: Disables git polling. When enabled only webhooks will
+                    be used.
+                  type: boolean
                 forceSyncGeneration:
                   description: Increment this number to force a redeployment of contents
                     from Git.
diff --git a/charts/fleet/templates/deployment_gitjob.yaml b/charts/fleet/templates/deployment_gitjob.yaml
index 936e8a69..6f8d9d92 100644
--- a/charts/fleet/templates/deployment_gitjob.yaml
+++ b/charts/fleet/templates/deployment_gitjob.yaml
@@ -14,12 +14,12 @@ spec:
     spec:
       serviceAccountName: gitjob
       containers:
-        - image: "{{ template "system_default_registry" . }}{{ .Values.gitjob.repository }}:{{ .Values.gitjob.tag }}"
+        - image: "{{ template "system_default_registry" . }}{{ .Values.image.repository }}:{{ .Values.image.tag }}"
           name: gitjob
           args:
           - gitjob
           - --gitjob-image
-          - "{{ template "system_default_registry" . }}{{ .Values.gitjob.repository }}:{{ .Values.gitjob.tag }}"
+          - "{{ template "system_default_registry" . }}{{ .Values.image.repository }}:{{ .Values.image.tag }}"
           {{- if .Values.debug }}
           - --debug
           {{- end }}
@@ -40,11 +40,11 @@ spec:
             - name: CATTLE_DEV_MODE
               value: "true"
           {{- end }}
-      {{- with .Values.gitjob.tolerations }}
+      {{- with .Values.tolerations }}
       tolerations:
         {{- toYaml . | nindent 8 }}
       {{- end }}
-      {{- with .Values.gitjob.nodeSelector }}
+      {{- with .Values.nodeSelector }}
       nodeSelector:
         {{- toYaml . | nindent 8 }}
       {{- end }}
diff --git a/charts/fleet/templates/job_cleanup_clusterregistrations.yaml b/charts/fleet/templates/job_cleanup_clusterregistrations.yaml
index 17d1ba78..a58ac6b4 100644
--- a/charts/fleet/templates/job_cleanup_clusterregistrations.yaml
+++ b/charts/fleet/templates/job_cleanup_clusterregistrations.yaml
@@ -21,7 +21,7 @@ spec:
         runAsUser: 1000
       containers:
       - name: cleanup
-        image: "{{ template "system_default_registry" . }}{{.Values.agentImage.repository}}:{{.Values.agentImage.tag}}"
+        image: "{{ template "system_default_registry" . }}{{.Values.image.repository}}:{{.Values.image.tag}}"
         imagePullPolicy: {{ .Values.global.imagePullPolicy }}
         securityContext:
           allowPrivilegeEscalation: false
diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml
index 8ff51483..e3604079 100644
--- a/charts/fleet/values.yaml
+++ b/charts/fleet/values.yaml
@@ -87,16 +87,3 @@ leaderElection:
   leaseDuration: 30s
   retryPeriod: 10s
   renewDeadline: 25s
-
-gitjob:
-  repository: rancher/gitjob
-  tag: dev
-  imagePullPolicy: IfNotPresent
-  nodeSelector:
-    kubernetes.io/os: linux
-
-  tolerations:
-    - key: cattle.io/os
-      operator: "Equal"
-      value: "linux"
-      effect: NoSchedule
diff --git a/cmd/gitjob/main.go b/cmd/gitjob/main.go
index 58bc6bec..891d52fd 100644
--- a/cmd/gitjob/main.go
+++ b/cmd/gitjob/main.go
@@ -72,7 +72,7 @@ func run(ctx context.Context) error {
 		Client:    mgr.GetClient(),
 		Scheme:    mgr.GetScheme(),
 		Image:     flags.image,
-		GitPoller: poll.NewHandler(mgr.GetClient()),
+		GitPoller: poll.NewHandler(ctx, mgr.GetClient()),
 		Log:       ctrl.Log.WithName("gitjob-reconciler"),
 	}
 
@@ -102,7 +102,7 @@ func bindFlags() flags {
 	flag.StringVar(
 		&image,
 		"gitjob-image",
-		"rancher/gitjob:dev",
+		"rancher/fleet:dev",
 		"The gitjob image that will be used in the generated job.",
 	)
 	flag.StringVar(&listen, "listen", ":8080", "The port the webhook listens.")
diff --git a/dev/build-fleet b/dev/build-fleet
index 053e7a32..be074da7 100755
--- a/dev/build-fleet
+++ b/dev/build-fleet
@@ -17,6 +17,10 @@ if ! git diff --quiet HEAD origin/master --  pkg/apis/fleet.cattle.io/v1alpha1;
 fi
 
 export GOOS=linux
+# gitjob
+go build -gcflags='all=-N -l' -o "bin/gitcloner-linux-$GOARCH" ./cmd/gitcloner
+go build -gcflags='all=-N -l' -o "bin/gitjob-linux-$GOARCH" ./cmd/gitjob
+
 # fleet
 go build -gcflags='all=-N -l' -o bin/fleetcontroller-linux-"$GOARCH" ./cmd/fleetcontroller
 docker build -f package/Dockerfile -t rancher/fleet:dev --build-arg="ARCH=$GOARCH"  .
@@ -25,9 +29,3 @@ docker build -f package/Dockerfile -t rancher/fleet:dev --build-arg="ARCH=$GOARC
 go build -gcflags='all=-N -l' -o "bin/fleet-linux-$GOARCH" ./cmd/fleetcli
 go build -gcflags='all=-N -l' -o "bin/fleetagent-linux-$GOARCH" ./cmd/fleetagent
 docker build -f package/Dockerfile.agent -t rancher/fleet-agent:dev --build-arg="ARCH=$GOARCH" .
-
-# gitjob
-go build -gcflags='all=-N -l' -o "bin/gitcloner-linux-$GOARCH" ./cmd/gitcloner
-go build -gcflags='all=-N -l' -o "bin/gitjob-linux-$GOARCH" ./cmd/gitjob
-
-docker build -f package/Dockerfile.gitjob -t rancher/gitjob:dev --build-arg="ARCH=$GOARCH" .
diff --git a/dev/import-images-k3d b/dev/import-images-k3d
index ab536edd..eed770ac 100755
--- a/dev/import-images-k3d
+++ b/dev/import-images-k3d
@@ -5,7 +5,7 @@ set -euxo pipefail
 upstream_ctx="${FLEET_E2E_CLUSTER-k3d-upstream}"
 downstream_ctx="${FLEET_E2E_CLUSTER_DOWNSTREAM-k3d-downstream}"
 
-k3d image import rancher/fleet:dev rancher/fleet-agent:dev rancher/gitjob:dev -m direct -c "${upstream_ctx#k3d-}"
+k3d image import rancher/fleet:dev rancher/fleet-agent:dev -m direct -c "${upstream_ctx#k3d-}"
 
 if [ "$upstream_ctx" != "$downstream_ctx" ]; then
   k3d image import rancher/fleet-agent:dev -m direct -c "${downstream_ctx#k3d-}"
diff --git a/docs/windows.md b/docs/windows.md
deleted file mode 100644
index 8b15f412..00000000
--- a/docs/windows.md
+++ /dev/null
@@ -1,36 +0,0 @@
-# Windows Development
-
-Fleet agents run natively on both Windows and Linux downstream Kubernetes nodes.
-While Fleet is currently unsupported for _local_ Windows clusters, we need to test its usage of _downstream_ Windows clusters.
-
-Once your Windows agent is built (following similar directions from Linux agent development above), start Rancher on a Kubernetes cluster and perform the following steps:
-
-1. Create a downstream RKE1 or RKE2 Windows cluster
-2. Wait for `fleet-agent` to be deployed downstream (native on Linux and Windows, but will likely default to Linux)
-3. Change `fleet-agent` Deployment image name and tag to be your custom agent image name and tag
-4. Change `fleet-agent` Deployment image `PullPolicy` to `Always`
-5. Delete the existing `fleet-agent` pod and wait for the new one to reach a `Running` state _(note: ensure there aren't any non-transient error logs)_
-6. Create the [multi-cluster/windows-helm](https://github.com/rancher/fleet-examples/tree/master/multi-cluster/windows-helm) GitRepo CR in the local cluster
-7. Observe "Active" or "Running" or "Completed" states for `gitrepos.fleet.cattle.io` and other resources (e.g. pods deployed from the chart)
-
-## Building `fleet-agent` for Windows
-
-Testing Windows images locally can be done on a Windows host.
-
-First, enter `powershell` and install Git.
-You can use [Chocolatey](https://chocolatey.org/install) to [install the package](https://chocolatey.org/packages/git).
-
-Next, clone the repository and checkout your branch.
-
-```powershell
-& 'C:\Program Files\Git\bin\git.exe' clone https://github.com/<user>/<fleet-fork>.git
-& 'C:\Program Files\Git\bin\git.exe' checkout --track origin/<dev-branch>
-```
-
-Finally, you can build the image with an uploaded binary from a tagged release.
-This is useful when testing the `fleet-agent` on a Windows cluster.
-
-```powershell
-docker build -t test -f package\Dockerfile-windows.agent --build-arg SERVERCORE_VERSION=<windows-version> --build-arg RELEASES=releases.rancher.com --build-arg VERSION=<fleet-tag> .
-```
-
diff --git a/go.mod b/go.mod
index ad85126f..580830fd 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@ require (
 	github.com/Masterminds/sprig/v3 v3.2.3
 	github.com/cheggaaa/pb v1.0.29
 	github.com/davecgh/go-spew v1.1.1
+	github.com/docker/docker v25.0.3+incompatible
 	github.com/evanphx/json-patch v5.7.0+incompatible
 	github.com/go-git/go-billy/v5 v5.5.0
 	github.com/go-git/go-git/v5 v5.11.0
@@ -33,14 +34,13 @@ require (
 	github.com/rancher/fleet/pkg/apis v0.0.0-00010101000000-000000000000
 	github.com/rancher/lasso v0.0.0-20230830164424-d684fdeb6f29
 	github.com/rancher/wrangler/v2 v2.1.2
-	github.com/reugn/go-quartz v0.11.1
+	github.com/reugn/go-quartz v0.11.2
 	github.com/sirupsen/logrus v1.9.3
 	github.com/spf13/cobra v1.8.0
 	github.com/stretchr/testify v1.8.4
 	github.com/testcontainers/testcontainers-go v0.28.0
 	go.uber.org/mock v0.4.0
 	golang.org/x/crypto v0.19.0
-	golang.org/x/exp v0.0.0-20240222234643-814bf88cf225
 	golang.org/x/sync v0.6.0
 	gopkg.in/go-playground/webhooks.v5 v5.17.0
 	gopkg.in/yaml.v2 v2.4.0
@@ -97,7 +97,6 @@ require (
 	github.com/distribution/reference v0.5.0 // indirect
 	github.com/docker/cli v25.0.1+incompatible // indirect
 	github.com/docker/distribution v2.8.3+incompatible // indirect
-	github.com/docker/docker v25.0.3+incompatible // indirect
 	github.com/docker/docker-credential-helpers v0.7.0 // indirect
 	github.com/docker/go-connections v0.5.0 // indirect
 	github.com/docker/go-metrics v0.0.1 // indirect
@@ -218,6 +217,7 @@ require (
 	go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.26.0 // indirect
+	golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
 	golang.org/x/mod v0.15.0 // indirect
 	golang.org/x/net v0.21.0 // indirect
 	golang.org/x/oauth2 v0.17.0 // indirect
diff --git a/go.sum b/go.sum
index 751bb7fb..102c010c 100644
--- a/go.sum
+++ b/go.sum
@@ -748,8 +748,8 @@ github.com/rancher/lasso v0.0.0-20230830164424-d684fdeb6f29 h1:+kige/h8/LnzWgPjB
 github.com/rancher/lasso v0.0.0-20230830164424-d684fdeb6f29/go.mod h1:kgk9kJVMj9FIrrXU0iyM6u/9Je4bEjPImqswkTVaKsQ=
 github.com/rancher/wrangler/v2 v2.1.2 h1:XAiReB3caOAepsTHs12AwKVv4vBJkjLosZPXbbj/85E=
 github.com/rancher/wrangler/v2 v2.1.2/go.mod h1:LgpEqT2ATL0MevUsL5rohrAEU419MIPeswsMNHw35d4=
-github.com/reugn/go-quartz v0.11.1 h1:T67ZeP0h2uRxtoKFZ1Lnbf3u4jh6PBgeBlw2BUk9rW0=
-github.com/reugn/go-quartz v0.11.1/go.mod h1:no4ktgYbAAuY0E1SchR8cTx1LF4jYIzdgaQhzRPSkpk=
+github.com/reugn/go-quartz v0.11.2 h1:+jc54Ji06n/D/endEPmc+CuG/Jc8466nda1oxtFRrks=
+github.com/reugn/go-quartz v0.11.2/go.mod h1:no4ktgYbAAuY0E1SchR8cTx1LF4jYIzdgaQhzRPSkpk=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
diff --git a/internal/cmd/controller/reconciler/gitrepo_controller.go b/internal/cmd/controller/reconciler/gitrepo_controller.go
index 6fab0a4b..7968056a 100644
--- a/internal/cmd/controller/reconciler/gitrepo_controller.go
+++ b/internal/cmd/controller/reconciler/gitrepo_controller.go
@@ -5,6 +5,7 @@ package reconciler
 import (
 	"context"
 	"fmt"
+	"reflect"
 
 	grutil "github.com/rancher/fleet/internal/cmd/controller/gitrepo"
 	"github.com/rancher/fleet/internal/cmd/controller/imagescan"
@@ -24,6 +25,7 @@ import (
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+	"sigs.k8s.io/controller-runtime/pkg/event"
 	"sigs.k8s.io/controller-runtime/pkg/handler"
 	"sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/controller-runtime/pkg/predicate"
@@ -41,7 +43,7 @@ type GitRepoReconciler struct {
 //+kubebuilder:rbac:groups=fleet.cattle.io,resources=gitrepos/status,verbs=get;update;patch
 //+kubebuilder:rbac:groups=fleet.cattle.io,resources=gitrepos/finalizers,verbs=update
 
-// Reconcile creates bundle deployments for a bundle
+// Reconcile creates resources for a GitRepo
 // nolint:gocyclo // creates multiple owned resources
 func (r *GitRepoReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	logger := log.FromContext(ctx).WithName("gitrepo")
@@ -86,16 +88,14 @@ func (r *GitRepoReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
 	}
 
 	// Refresh the status
-	if gitrepo.DeletionTimestamp != nil {
-		err = grutil.SetStatusFromBundleDeployments(ctx, r.Client, gitrepo)
-		if err != nil {
-			return ctrl.Result{}, r.updateErrorStatus(ctx, req.NamespacedName, gitrepo.Status, err)
-		}
+	err = grutil.SetStatusFromBundleDeployments(ctx, r.Client, gitrepo)
+	if err != nil {
+		return ctrl.Result{}, r.updateErrorStatus(ctx, req.NamespacedName, gitrepo.Status, err)
+	}
 
-		err = grutil.SetStatusFromBundles(ctx, r.Client, gitrepo)
-		if err != nil {
-			return ctrl.Result{}, r.updateErrorStatus(ctx, req.NamespacedName, gitrepo.Status, err)
-		}
+	err = grutil.SetStatusFromBundles(ctx, r.Client, gitrepo)
+	if err != nil {
+		return ctrl.Result{}, r.updateErrorStatus(ctx, req.NamespacedName, gitrepo.Status, err)
 	}
 
 	// Ideally, this should be done in the git job reconciler, but setting the status from bundle deployments
@@ -225,8 +225,9 @@ func (r *GitRepoReconciler) SetupWithManager(mgr ctrl.Manager) error {
 			}),
 		).
 		WithEventFilter(
-			// do not trigger for status changes
+			// do not trigger for GitRepo status changes
 			predicate.Or(
+				bundleStatusChangedPredicate(),
 				predicate.GenerationChangedPredicate{},
 				predicate.AnnotationChangedPredicate{},
 				predicate.LabelChangedPredicate{},
@@ -235,6 +236,27 @@ func (r *GitRepoReconciler) SetupWithManager(mgr ctrl.Manager) error {
 		Complete(r)
 }
 
+// bundleStatusChangedPredicate returns true if the bundle
+// status has changed, or the bundle was created
+func bundleStatusChangedPredicate() predicate.Funcs {
+	return predicate.Funcs{
+		CreateFunc: func(e event.CreateEvent) bool {
+			return true
+		},
+		UpdateFunc: func(e event.UpdateEvent) bool {
+			n, isBundle := e.ObjectNew.(*fleet.Bundle)
+			if !isBundle {
+				return false
+			}
+			o := e.ObjectOld.(*fleet.Bundle)
+			if n == nil || o == nil {
+				return false
+			}
+			return !reflect.DeepEqual(n.Status, o.Status)
+		},
+	}
+}
+
 func purgeBundles(ctx context.Context, c client.Client, gitrepo types.NamespacedName) error {
 	bundles := &fleet.BundleList{}
 	err := c.List(ctx, bundles, client.MatchingLabels{fleet.RepoLabel: gitrepo.Name}, client.InNamespace(gitrepo.Namespace))
diff --git a/internal/cmd/controller/reconciler/imagescan_controller.go b/internal/cmd/controller/reconciler/imagescan_controller.go
index db80ff00..c62ea88d 100644
--- a/internal/cmd/controller/reconciler/imagescan_controller.go
+++ b/internal/cmd/controller/reconciler/imagescan_controller.go
@@ -60,6 +60,7 @@ func (r *ImageScanReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
 	// error if the job does not exist, which we ignore.
 	tagScanKey := imagescan.TagScanKey(req.Namespace, req.Name)
 	_ = r.Scheduler.DeleteJob(tagScanKey)
+
 	err = r.Scheduler.ScheduleJob(
 		quartz.NewJobDetail(
 			imagescan.NewTagScanJob(r.Client, req.Namespace, req.Name),
diff --git a/internal/cmd/gitjob/gitjob.go b/internal/cmd/gitjob/gitjob.go
index deb99d82..78c48261 100644
--- a/internal/cmd/gitjob/gitjob.go
+++ b/internal/cmd/gitjob/gitjob.go
@@ -47,8 +47,8 @@ const (
 var two = int32(2)
 
 type GitPoller interface {
-	AddOrModifyGitRepoWatch(ctx context.Context, gitRepo v1alpha1.GitRepo)
-	CleanUpWatches(ctx context.Context)
+	AddOrModifyGitRepoPollJob(ctx context.Context, gitRepo v1alpha1.GitRepo)
+	CleanUpGitRepoPollJobs(ctx context.Context)
 }
 
 // CronJobReconciler reconciles a GitJob object
@@ -71,7 +71,7 @@ func (r *GitJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
 // Reconcile is part of the main kubernetes reconciliation loop which aims to
 // move the current state of the cluster closer to the desired state.
 // The Reconcile function compares the state specified by
-// the GitJob object against the actual cluster state, and then
+// the GitRepo object against the actual cluster state, and then
 // performs operations to make the cluster state reflect the state specified by
 // the user.
 //
@@ -83,11 +83,11 @@ func (r *GitJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 	if err := r.Get(ctx, req.NamespacedName, &gitRepo); err != nil && !errors.IsNotFound(err) {
 		return ctrl.Result{}, err
 	} else if errors.IsNotFound(err) {
-		r.GitPoller.CleanUpWatches(ctx)
+		r.GitPoller.CleanUpGitRepoPollJobs(ctx)
 		return ctrl.Result{}, nil
 	}
 
-	r.GitPoller.AddOrModifyGitRepoWatch(ctx, gitRepo)
+	r.GitPoller.AddOrModifyGitRepoPollJob(ctx, gitRepo)
 
 	var job batchv1.Job
 	err := r.Get(ctx, types.NamespacedName{
@@ -380,7 +380,7 @@ func (r *GitJobReconciler) computeJobSpec(ctx context.Context, gitrepo *v1alpha1
 				Containers: []corev1.Container{
 					{
 						Name:         "fleet",
-						Image:        config.DefaultAgentImage,
+						Image:        config.DefaultManagerImage,
 						Command:      []string{"log.sh"},
 						Args:         append(args, paths...),
 						WorkingDir:   "/workspace/source",
diff --git a/internal/mocks/poller_mock.go b/internal/mocks/poller_mock.go
index 96a2fb19..7ad617b9 100644
--- a/internal/mocks/poller_mock.go
+++ b/internal/mocks/poller_mock.go
@@ -40,26 +40,26 @@ func (m *MockGitPoller) EXPECT() *MockGitPollerMockRecorder {
 	return m.recorder
 }
 
-// AddOrModifyGitRepoWatch mocks base method.
-func (m *MockGitPoller) AddOrModifyGitRepoWatch(arg0 context.Context, arg1 v1alpha1.GitRepo) {
+// AddOrModifyGitRepoPollJob mocks base method.
+func (m *MockGitPoller) AddOrModifyGitRepoPollJob(arg0 context.Context, arg1 v1alpha1.GitRepo) {
 	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "AddOrModifyGitRepoWatch", arg0, arg1)
+	m.ctrl.Call(m, "AddOrModifyGitRepoPollJob", arg0, arg1)
 }
 
-// AddOrModifyGitRepoWatch indicates an expected call of AddOrModifyGitRepoWatch.
-func (mr *MockGitPollerMockRecorder) AddOrModifyGitRepoWatch(arg0, arg1 any) *gomock.Call {
+// AddOrModifyGitRepoPollJob indicates an expected call of AddOrModifyGitRepoPollJob.
+func (mr *MockGitPollerMockRecorder) AddOrModifyGitRepoPollJob(arg0, arg1 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddOrModifyGitRepoWatch", reflect.TypeOf((*MockGitPoller)(nil).AddOrModifyGitRepoWatch), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddOrModifyGitRepoPollJob", reflect.TypeOf((*MockGitPoller)(nil).AddOrModifyGitRepoPollJob), arg0, arg1)
 }
 
-// CleanUpWatches mocks base method.
-func (m *MockGitPoller) CleanUpWatches(arg0 context.Context) {
+// CleanUpGitRepoPollJobs mocks base method.
+func (m *MockGitPoller) CleanUpGitRepoPollJobs(arg0 context.Context) {
 	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "CleanUpWatches", arg0)
+	m.ctrl.Call(m, "CleanUpGitRepoPollJobs", arg0)
 }
 
-// CleanUpWatches indicates an expected call of CleanUpWatches.
-func (mr *MockGitPollerMockRecorder) CleanUpWatches(arg0 any) *gomock.Call {
+// CleanUpGitRepoPollJobs indicates an expected call of CleanUpGitRepoPollJobs.
+func (mr *MockGitPollerMockRecorder) CleanUpGitRepoPollJobs(arg0 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanUpWatches", reflect.TypeOf((*MockGitPoller)(nil).CleanUpWatches), arg0)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanUpGitRepoPollJobs", reflect.TypeOf((*MockGitPoller)(nil).CleanUpGitRepoPollJobs), arg0)
 }
diff --git a/manifest-agent.tmpl b/manifest-agent.tmpl
deleted file mode 100644
index 97eb43ec..00000000
--- a/manifest-agent.tmpl
+++ /dev/null
@@ -1,24 +0,0 @@
-image: rancher/fleet-agent:{{#if build.tag}}{{build.tag}}{{else}}{{replace "release/" "" build.branch }}-head{{/if}}
-manifests:
-  -
-    image: rancher/fleet-agent:{{#if build.tag}}{{build.tag}}{{else}}{{replace "release/" "" build.branch }}-head{{/if}}-linux-amd64
-    platform:
-      architecture: amd64
-      os: linux
-  -
-    image: rancher/fleet-agent:{{#if build.tag}}{{build.tag}}{{else}}{{replace "release/" "" build.branch }}-head{{/if}}-linux-arm64
-    platform:
-      architecture: arm64
-      os: linux
-  -
-    image: rancher/fleet-agent:{{#if build.tag}}{{build.tag}}{{else}}{{replace "release/" "" build.branch }}-head{{/if}}-windows-1809
-    platform:
-      architecture: amd64
-      os: windows
-      version: 1809
-  -
-    image: rancher/fleet-agent:{{#if build.tag}}{{build.tag}}{{else}}{{replace "release/" "" build.branch }}-head{{/if}}-windows-ltsc2022
-    platform:
-      architecture: amd64
-      os: windows
-      version: ltsc2022
diff --git a/manifest-gitjob.tmpl b/manifest-gitjob.tmpl
deleted file mode 100644
index 5ad6abf5..00000000
--- a/manifest-gitjob.tmpl
+++ /dev/null
@@ -1,12 +0,0 @@
-image: rancher/gitjob:{{#if build.tag}}{{build.tag}}{{else}}{{replace "release/" "" build.branch }}-head{{/if}}
-manifests:
-  -
-    image: rancher/gitjob:{{#if build.tag}}{{build.tag}}{{else}}{{replace "release/" "" build.branch }}-head{{/if}}-linux-amd64
-    platform:
-      architecture: amd64
-      os: linux
-  -
-    image: rancher/gitjob:{{#if build.tag}}{{build.tag}}{{else}}{{replace "release/" "" build.branch }}-head{{/if}}-linux-arm64
-    platform:
-      architecture: arm64
-      os: linux
diff --git a/manifest.tmpl b/manifest.tmpl
deleted file mode 100644
index 23491676..00000000
--- a/manifest.tmpl
+++ /dev/null
@@ -1,12 +0,0 @@
-image: rancher/fleet:{{#if build.tag}}{{build.tag}}{{else}}{{replace "release/" "" build.branch }}-head{{/if}}
-manifests:
-  -
-    image: rancher/fleet:{{#if build.tag}}{{build.tag}}{{else}}{{replace "release/" "" build.branch }}-head{{/if}}-linux-amd64
-    platform:
-      architecture: amd64
-      os: linux
-  -
-    image: rancher/fleet:{{#if build.tag}}{{build.tag}}{{else}}{{replace "release/" "" build.branch }}-head{{/if}}-linux-arm64
-    platform:
-      architecture: arm64
-      os: linux
diff --git a/package/Dockerfile b/package/Dockerfile
index d40ac464..824de1ec 100644
--- a/package/Dockerfile
+++ b/package/Dockerfile
@@ -1,21 +1,37 @@
 ARG BUILD_ENV=dapper
 ARG ARCH
 
-FROM --platform=linux/$ARCH registry.suse.com/bci/bci-busybox:15.5 AS base
+FROM --platform=linux/$ARCH registry.suse.com/bci/bci-base:15.5 AS base
+COPY package/log.sh /usr/bin/
+RUN zypper -n update && \
+    zypper -n install openssh catatonit git-core && \
+    zypper -n clean -a
+RUN groupadd -g 1000 fleet-apply
+RUN useradd -u 1000 -U -m gitjob
 
 FROM base AS copy_dapper
 ONBUILD ARG ARCH
-ONBUILD COPY bin/fleetcontroller-linux-${ARCH} /usr/bin/fleetcontroller
+ONBUILD COPY bin/fleetcontroller-linux-$ARCH /usr/bin/fleetcontroller
+ONBUILD COPY bin/fleet-linux-$ARCH /usr/bin/fleet
+ONBUILD COPY bin/gitjob-linux-$ARCH /usr/bin/gitjob
+ONBUILD COPY bin/gitcloner-linux-$ARCH /usr/bin/gitcloner
 
 FROM base AS copy_buildx
 ONBUILD ARG TARGETARCH
-ONBUILD COPY bin/fleetcontroller-linux-${TARGETARCH} /usr/bin/fleetcontroller
+ONBUILD COPY bin/fleetcontroller-linux-$TARGETARCH /usr/bin/fleetcontroller
+ONBUILD COPY bin/fleet-linux-$TARGETARCH /usr/bin/fleet
+ONBUILD COPY bin/gitjob-linux-$TARGETARCH /usr/bin/gitjob
+ONBUILD COPY bin/gitcloner-linux-$TARGETARCH /usr/bin/gitcloner
 
 FROM base AS copy_goreleaser
 ONBUILD ARG ARCH
-ONBUILD COPY fleetcontroller-linux-${ARCH} /usr/bin/fleetcontroller
+ONBUILD COPY fleetcontroller-linux-$ARCH /usr/bin/fleetcontroller
+ONBUILD COPY fleet-linux-$ARCH /usr/bin/fleet
+ONBUILD COPY gitjob-linux-$ARCH /usr/bin/gitjob
+ONBUILD COPY gitcloner-linux-$ARCH /usr/bin/gitcloner
 
 FROM copy_${BUILD_ENV}
-RUN addgroup -g 1000 fleet-apply && adduser -u 1000 -G fleet-apply -D fleet-apply
+RUN useradd fleet-apply -u 1001 -G fleet-apply
 USER 1000
+ENTRYPOINT ["catatonit", "--"]
 CMD ["fleetcontroller"]
diff --git a/package/Dockerfile-windows.agent b/package/Dockerfile-windows.agent
deleted file mode 100644
index 3fdd7620..00000000
--- a/package/Dockerfile-windows.agent
+++ /dev/null
@@ -1,28 +0,0 @@
-ARG SERVERCORE_VERSION
-FROM mcr.microsoft.com/windows/servercore:${SERVERCORE_VERSION}
-ARG RELEASES
-ARG VERSION
-ENV RELEASES=$RELEASES
-ENV VERSION=$VERSION
-SHELL ["powershell", "-NoLogo", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
-RUN pushd c:\; \
-    $URL = 'https://github.com/git-for-windows/git/releases/download/v2.42.0.windows.2/MinGit-2.42.0.2-64-bit.zip'; \
-    \
-    Write-Host ('Downloading git from {0} ...' -f $URL); \
-    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; \
-    Invoke-WebRequest -UseBasicParsing -OutFile c:\git.zip -Uri $URL; \
-    \
-    Write-Host 'Expanding ...'; \
-    Expand-Archive -Force -Path c:\git.zip -DestinationPath c:\git\.; \
-    \
-    Write-Host 'Cleaning ...'; \
-    Remove-Item -Force -Recurse -Path c:\git.zip; \
-    \
-    Write-Host 'Complete.'; \
-    popd;
-RUN $URL = 'https://{0}/fleet/{1}/fleetagent-windows-amd64.exe' -f $env:RELEASES, $env:VERSION; \
-    New-Item -Path 'c:\\' -Name 'fleet' -ItemType 'directory'; \
-    Write-Host ('Downloading dapper from {0} ...' -f $URL); \
-    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; \
-    Invoke-WebRequest -UseBasicParsing -OutFile c:/fleet/fleetagent-windows.exe -Uri $URL;
-CMD ["C:\\fleet\\fleetagent-windows.exe"]
diff --git a/package/Dockerfile.agent b/package/Dockerfile.agent
index 98f89229..c8f9d5de 100644
--- a/package/Dockerfile.agent
+++ b/package/Dockerfile.agent
@@ -1,8 +1,7 @@
 ARG BUILD_ENV=dapper
 ARG ARCH
 
-FROM --platform=linux/$ARCH registry.suse.com/suse/git:2.35 AS base
-COPY package/log.sh /usr/bin/
+FROM --platform=linux/$ARCH registry.suse.com/bci/bci-busybox:15.5 AS base
 # Create non-root user and group
 RUN echo "fleet-apply:x:1000:1000::/home/fleet-apply:/bin/bash" >> /etc/passwd && \
     echo "fleet-apply:x:1000:" >> /etc/group && \
@@ -12,12 +11,10 @@ RUN echo "fleet-apply:x:1000:1000::/home/fleet-apply:/bin/bash" >> /etc/passwd &
 FROM base AS copy_dapper
 ONBUILD ARG ARCH
 ONBUILD COPY bin/fleetagent-linux-$ARCH /usr/bin/fleetagent
-ONBUILD COPY bin/fleet-linux-$ARCH /usr/bin/fleet
 
 FROM base AS copy_buildx
 ONBUILD ARG TARGETARCH
 ONBUILD COPY bin/fleetagent-linux-$TARGETARCH /usr/bin/fleetagent
-ONBUILD COPY bin/fleet-linux-$TARGETARCH /usr/bin/fleet
 
 FROM base AS copy_goreleaser
 ONBUILD ARG ARCH
diff --git a/package/Dockerfile.gitjob b/package/Dockerfile.gitjob
deleted file mode 100644
index 2829cc61..00000000
--- a/package/Dockerfile.gitjob
+++ /dev/null
@@ -1,28 +0,0 @@
-ARG BUILD_ENV=dapper
-ARG ARCH
-
-FROM --platform=linux/$ARCH registry.suse.com/bci/bci-base:15.5 AS base
-RUN zypper -n update && \
-    zypper -n install openssh catatonit git-core && \
-    zypper -n clean -a
-RUN useradd -u 1000 -U -m gitjob
-
-FROM base AS copy_dapper
-ONBUILD ARG ARCH
-ONBUILD COPY bin/gitjob-linux-$ARCH /usr/bin/gitjob
-ONBUILD COPY bin/gitcloner-linux-$ARCH /usr/bin/gitcloner
-
-FROM base AS copy_buildx
-ONBUILD ARG TARGETARCH
-ONBUILD COPY bin/gitjob-linux-$TARGETARCH /usr/bin/gitjob
-ONBUILD COPY bin/gitcloner-linux-$TARGETARCH /usr/bin/gitcloner
-
-FROM base AS copy_goreleaser
-ONBUILD ARG ARCH
-ONBUILD COPY gitjob-linux-$ARCH /usr/bin/gitjob
-ONBUILD COPY gitcloner-linux-$ARCH /usr/bin/gitcloner
-
-FROM copy_${BUILD_ENV}
-USER 1000
-ENTRYPOINT ["catatonit", "--"]
-CMD ["gitjob"]
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/gitrepo_types.go b/pkg/apis/fleet.cattle.io/v1alpha1/gitrepo_types.go
index 9af7d5d7..a0eb49c9 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/gitrepo_types.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/gitrepo_types.go
@@ -129,6 +129,9 @@ type GitRepoSpec struct {
 
 	// CorrectDrift specifies how drift correction should work.
 	CorrectDrift *CorrectDrift `json:"correctDrift,omitempty"`
+
+	// Disables git polling. When enabled only webhooks will be used.
+	DisablePolling bool `json:"disablePolling,omitempty"`
 }
 
 // GitTarget is a cluster or cluster group to deploy to.
diff --git a/pkg/git/mocks/scheduledjob_mock.go b/pkg/git/mocks/scheduledjob_mock.go
new file mode 100644
index 00000000..d43cda0f
--- /dev/null
+++ b/pkg/git/mocks/scheduledjob_mock.go
@@ -0,0 +1,23 @@
+package mocks
+
+import (
+	"time"
+
+	quartz "github.com/reugn/go-quartz/quartz"
+)
+
+type MockScheduledJob struct {
+	Detail          *quartz.JobDetail
+	TriggerDuration time.Duration
+}
+
+func (m *MockScheduledJob) JobDetail() *quartz.JobDetail {
+	return m.Detail
+}
+
+func (m *MockScheduledJob) Trigger() quartz.Trigger {
+	return quartz.NewSimpleTrigger(m.TriggerDuration)
+}
+func (m *MockScheduledJob) NextRunTime() int64 {
+	return 0
+}
diff --git a/pkg/git/mocks/scheduler_mock.go b/pkg/git/mocks/scheduler_mock.go
new file mode 100644
index 00000000..02b797af
--- /dev/null
+++ b/pkg/git/mocks/scheduler_mock.go
@@ -0,0 +1,188 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/reugn/go-quartz/quartz (interfaces: Scheduler)
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+        context "context"
+        gomock "go.uber.org/mock/gomock"
+        quartz "github.com/reugn/go-quartz/quartz"
+        reflect "reflect"
+)
+
+// MockScheduler is a mock of Scheduler interface.
+type MockScheduler struct {
+        ctrl     *gomock.Controller
+        recorder *MockSchedulerMockRecorder
+}
+
+// MockSchedulerMockRecorder is the mock recorder for MockScheduler.
+type MockSchedulerMockRecorder struct {
+        mock *MockScheduler
+}
+
+// NewMockScheduler creates a new mock instance.
+func NewMockScheduler(ctrl *gomock.Controller) *MockScheduler {
+        mock := &MockScheduler{ctrl: ctrl}
+        mock.recorder = &MockSchedulerMockRecorder{mock}
+        return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockScheduler) EXPECT() *MockSchedulerMockRecorder {
+        return m.recorder
+}
+
+// Clear mocks base method.
+func (m *MockScheduler) Clear() error {
+        m.ctrl.T.Helper()
+        ret := m.ctrl.Call(m, "Clear")
+        ret0, _ := ret[0].(error)
+        return ret0
+}
+
+// Clear indicates an expected call of Clear.
+func (mr *MockSchedulerMockRecorder) Clear() *gomock.Call {
+        mr.mock.ctrl.T.Helper()
+        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Clear", reflect.TypeOf((*MockScheduler)(nil).Clear))
+}
+
+// DeleteJob mocks base method.
+func (m *MockScheduler) DeleteJob(arg0 *quartz.JobKey) error {
+        m.ctrl.T.Helper()
+        ret := m.ctrl.Call(m, "DeleteJob", arg0)
+        ret0, _ := ret[0].(error)
+        return ret0
+}
+
+// DeleteJob indicates an expected call of DeleteJob.
+func (mr *MockSchedulerMockRecorder) DeleteJob(arg0 interface{}) *gomock.Call {
+        mr.mock.ctrl.T.Helper()
+        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteJob", reflect.TypeOf((*MockScheduler)(nil).DeleteJob), arg0)
+}
+
+// GetJobKeys mocks base method.
+func (m *MockScheduler) GetJobKeys(arg0 ...quartz.Matcher[quartz.ScheduledJob]) []*quartz.JobKey {
+        m.ctrl.T.Helper()
+        varargs := []interface{}{}
+        for _, a := range arg0 {
+                varargs = append(varargs, a)
+        }
+        ret := m.ctrl.Call(m, "GetJobKeys", varargs...)
+        ret0, _ := ret[0].([]*quartz.JobKey)
+        return ret0
+}
+
+// GetJobKeys indicates an expected call of GetJobKeys.
+func (mr *MockSchedulerMockRecorder) GetJobKeys(arg0 ...interface{}) *gomock.Call {
+        mr.mock.ctrl.T.Helper()
+        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetJobKeys", reflect.TypeOf((*MockScheduler)(nil).GetJobKeys), arg0...)
+}
+
+// GetScheduledJob mocks base method.
+func (m *MockScheduler) GetScheduledJob(arg0 *quartz.JobKey) (quartz.ScheduledJob, error) {
+        m.ctrl.T.Helper()
+        ret := m.ctrl.Call(m, "GetScheduledJob", arg0)
+        ret0, _ := ret[0].(quartz.ScheduledJob)
+        ret1, _ := ret[1].(error)
+        return ret0, ret1
+}
+
+// GetScheduledJob indicates an expected call of GetScheduledJob.
+func (mr *MockSchedulerMockRecorder) GetScheduledJob(arg0 interface{}) *gomock.Call {
+        mr.mock.ctrl.T.Helper()
+        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetScheduledJob", reflect.TypeOf((*MockScheduler)(nil).GetScheduledJob), arg0)
+}
+
+// IsStarted mocks base method.
+func (m *MockScheduler) IsStarted() bool {
+        m.ctrl.T.Helper()
+        ret := m.ctrl.Call(m, "IsStarted")
+        ret0, _ := ret[0].(bool)
+        return ret0
+}
+
+// IsStarted indicates an expected call of IsStarted.
+func (mr *MockSchedulerMockRecorder) IsStarted() *gomock.Call {
+        mr.mock.ctrl.T.Helper()
+        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsStarted", reflect.TypeOf((*MockScheduler)(nil).IsStarted))
+}
+
+// PauseJob mocks base method.
+func (m *MockScheduler) PauseJob(arg0 *quartz.JobKey) error {
+        m.ctrl.T.Helper()
+        ret := m.ctrl.Call(m, "PauseJob", arg0)
+        ret0, _ := ret[0].(error)
+        return ret0
+}
+
+// PauseJob indicates an expected call of PauseJob.
+func (mr *MockSchedulerMockRecorder) PauseJob(arg0 interface{}) *gomock.Call {
+        mr.mock.ctrl.T.Helper()
+        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PauseJob", reflect.TypeOf((*MockScheduler)(nil).PauseJob), arg0)
+}
+
+// ResumeJob mocks base method.
+func (m *MockScheduler) ResumeJob(arg0 *quartz.JobKey) error {
+        m.ctrl.T.Helper()
+        ret := m.ctrl.Call(m, "ResumeJob", arg0)
+        ret0, _ := ret[0].(error)
+        return ret0
+}
+
+// ResumeJob indicates an expected call of ResumeJob.
+func (mr *MockSchedulerMockRecorder) ResumeJob(arg0 interface{}) *gomock.Call {
+        mr.mock.ctrl.T.Helper()
+        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResumeJob", reflect.TypeOf((*MockScheduler)(nil).ResumeJob), arg0)
+}
+
+// ScheduleJob mocks base method.
+func (m *MockScheduler) ScheduleJob(arg0 *quartz.JobDetail, arg1 quartz.Trigger) error {
+        m.ctrl.T.Helper()
+        ret := m.ctrl.Call(m, "ScheduleJob", arg0, arg1)
+        ret0, _ := ret[0].(error)
+        return ret0
+}
+
+// ScheduleJob indicates an expected call of ScheduleJob.
+func (mr *MockSchedulerMockRecorder) ScheduleJob(arg0, arg1 interface{}) *gomock.Call {
+        mr.mock.ctrl.T.Helper()
+        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScheduleJob", reflect.TypeOf((*MockScheduler)(nil).ScheduleJob), arg0, arg1)
+}
+
+// Start mocks base method.
+func (m *MockScheduler) Start(arg0 context.Context) {
+        m.ctrl.T.Helper()
+        m.ctrl.Call(m, "Start", arg0)
+}
+
+// Start indicates an expected call of Start.
+func (mr *MockSchedulerMockRecorder) Start(arg0 interface{}) *gomock.Call {
+        mr.mock.ctrl.T.Helper()
+        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockScheduler)(nil).Start), arg0)
+}
+
+// Stop mocks base method.
+func (m *MockScheduler) Stop() {
+        m.ctrl.T.Helper()
+        m.ctrl.Call(m, "Stop")
+}
+
+// Stop indicates an expected call of Stop.
+func (mr *MockSchedulerMockRecorder) Stop() *gomock.Call {
+        mr.mock.ctrl.T.Helper()
+        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockScheduler)(nil).Stop))
+}
+
+// Wait mocks base method.
+func (m *MockScheduler) Wait(arg0 context.Context) {
+        m.ctrl.T.Helper()
+        m.ctrl.Call(m, "Wait", arg0)
+}
+
+// Wait indicates an expected call of Wait.
+func (mr *MockSchedulerMockRecorder) Wait(arg0 interface{}) *gomock.Call {
+        mr.mock.ctrl.T.Helper()
+        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wait", reflect.TypeOf((*MockScheduler)(nil).Wait), arg0)
+}
diff --git a/pkg/git/mocks/watch_mock.go b/pkg/git/mocks/watch_mock.go
deleted file mode 100644
index 40ed09e6..00000000
--- a/pkg/git/mocks/watch_mock.go
+++ /dev/null
@@ -1,103 +0,0 @@
-// Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/rancher/fleet/pkg/git/poll (interfaces: Watcher)
-//
-// Generated by this command:
-//
-//	mockgen --build_flags=--mod=mod -destination=../mocks/watch_mock.go -package=mocks github.com/rancher/fleet/pkg/git/poll Watcher
-//
-
-// Package mocks is a generated GoMock package.
-package mocks
-
-import (
-	context "context"
-	reflect "reflect"
-
-	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
-	gomock "go.uber.org/mock/gomock"
-)
-
-// MockWatcher is a mock of Watcher interface.
-type MockWatcher struct {
-	ctrl     *gomock.Controller
-	recorder *MockWatcherMockRecorder
-}
-
-// MockWatcherMockRecorder is the mock recorder for MockWatcher.
-type MockWatcherMockRecorder struct {
-	mock *MockWatcher
-}
-
-// NewMockWatcher creates a new mock instance.
-func NewMockWatcher(ctrl *gomock.Controller) *MockWatcher {
-	mock := &MockWatcher{ctrl: ctrl}
-	mock.recorder = &MockWatcherMockRecorder{mock}
-	return mock
-}
-
-// EXPECT returns an object that allows the caller to indicate expected use.
-func (m *MockWatcher) EXPECT() *MockWatcherMockRecorder {
-	return m.recorder
-}
-
-// Finish mocks base method.
-func (m *MockWatcher) Finish() {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "Finish")
-}
-
-// Finish indicates an expected call of Finish.
-func (mr *MockWatcherMockRecorder) Finish() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Finish", reflect.TypeOf((*MockWatcher)(nil).Finish))
-}
-
-// GetSyncInterval mocks base method.
-func (m *MockWatcher) GetSyncInterval() float64 {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetSyncInterval")
-	ret0, _ := ret[0].(float64)
-	return ret0
-}
-
-// GetSyncInterval indicates an expected call of GetSyncInterval.
-func (mr *MockWatcherMockRecorder) GetSyncInterval() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSyncInterval", reflect.TypeOf((*MockWatcher)(nil).GetSyncInterval))
-}
-
-// Restart mocks base method.
-func (m *MockWatcher) Restart(arg0 context.Context) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "Restart", arg0)
-}
-
-// Restart indicates an expected call of Restart.
-func (mr *MockWatcherMockRecorder) Restart(arg0 any) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Restart", reflect.TypeOf((*MockWatcher)(nil).Restart), arg0)
-}
-
-// StartBackgroundSync mocks base method.
-func (m *MockWatcher) StartBackgroundSync(arg0 context.Context) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "StartBackgroundSync", arg0)
-}
-
-// StartBackgroundSync indicates an expected call of StartBackgroundSync.
-func (mr *MockWatcherMockRecorder) StartBackgroundSync(arg0 any) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartBackgroundSync", reflect.TypeOf((*MockWatcher)(nil).StartBackgroundSync), arg0)
-}
-
-// UpdateGitRepo mocks base method.
-func (m *MockWatcher) UpdateGitRepo(arg0 v1alpha1.GitRepo) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "UpdateGitRepo", arg0)
-}
-
-// UpdateGitRepo indicates an expected call of UpdateGitRepo.
-func (mr *MockWatcherMockRecorder) UpdateGitRepo(arg0 any) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateGitRepo", reflect.TypeOf((*MockWatcher)(nil).UpdateGitRepo), arg0)
-}
diff --git a/pkg/git/poll/gitrepopolljob.go b/pkg/git/poll/gitrepopolljob.go
new file mode 100644
index 00000000..3fe05b20
--- /dev/null
+++ b/pkg/git/poll/gitrepopolljob.go
@@ -0,0 +1,86 @@
+// Copyright (c) 2021-2024 SUSE LLC
+package poll
+
+import (
+	"context"
+	"fmt"
+
+	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+	"github.com/reugn/go-quartz/quartz"
+	"golang.org/x/sync/semaphore"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/util/retry"
+
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+var _ quartz.Job = &GitRepoPollJob{}
+
+type GitFetcher interface {
+	LatestCommit(ctx context.Context, gitrepo *v1alpha1.GitRepo, client client.Client) (string, error)
+}
+
+type GitRepoPollJob struct {
+	sem     *semaphore.Weighted
+	client  client.Client
+	GitRepo v1alpha1.GitRepo
+	fetcher GitFetcher
+}
+
+func GitRepoPollKey(gitRepo v1alpha1.GitRepo) *quartz.JobKey {
+	return quartz.NewJobKeyWithGroup(gitRepo.Name, gitRepo.Namespace)
+}
+
+func NewGitRepoPollJob(c client.Client, f GitFetcher, gitRepo v1alpha1.GitRepo) *GitRepoPollJob {
+	return &GitRepoPollJob{
+		sem:     semaphore.NewWeighted(1),
+		client:  c,
+		GitRepo: gitRepo,
+		fetcher: f,
+	}
+}
+
+func (j *GitRepoPollJob) Execute(ctx context.Context) error {
+	if !j.sem.TryAcquire(1) {
+		// already running
+		return nil
+	}
+	defer j.sem.Release(1)
+
+	j.fetchLatestCommitAndUpdateStatus(ctx)
+
+	return nil
+}
+
+func (j *GitRepoPollJob) Description() string {
+	return j.String()
+}
+
+func (j *GitRepoPollJob) String() string {
+	return fmt.Sprintf("gitrepo-%s-%s", j.GitRepo.Namespace, j.GitRepo.Name)
+}
+
+func (j *GitRepoPollJob) fetchLatestCommitAndUpdateStatus(ctx context.Context) {
+	logger := ctrl.Log.WithName("git-latest-commit-poll-watch")
+	commit, err := j.fetcher.LatestCommit(ctx, &j.GitRepo, j.client)
+	if err != nil {
+		logger.Error(err, "error fetching commit", "gitrepo", j.GitRepo)
+		return
+	}
+	if j.GitRepo.Status.Commit != commit {
+		logger.Info("new commit found", "gitrepo", j.GitRepo, "commit", commit)
+		if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
+			var gitRepoFromCluster v1alpha1.GitRepo
+			err := j.client.Get(ctx, types.NamespacedName{Name: j.GitRepo.Name, Namespace: j.GitRepo.Namespace}, &gitRepoFromCluster)
+			if err != nil {
+				return err
+			}
+			gitRepoFromCluster.Status.Commit = commit
+
+			return j.client.Status().Update(ctx, &gitRepoFromCluster)
+		}); err != nil {
+			logger.Error(err, "error updating status when a new commit was found by polling", "gitrepo", j.GitRepo)
+		}
+	}
+}
diff --git a/pkg/git/poll/handler.go b/pkg/git/poll/handler.go
index 5de6305b..df55337e 100644
--- a/pkg/git/poll/handler.go
+++ b/pkg/git/poll/handler.go
@@ -2,10 +2,12 @@ package poll
 
 import (
 	"context"
-	"fmt"
-	"strings"
+	stderrors "errors"
+	"time"
 
 	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+	"github.com/rancher/fleet/pkg/git"
+	"github.com/reugn/go-quartz/quartz"
 
 	"github.com/go-logr/logr"
 	"k8s.io/apimachinery/pkg/api/errors"
@@ -14,6 +16,11 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
+const (
+	maxSchedulerOperationRetries = 3
+	defaultSyncInterval          = 15 * time.Second
+)
+
 type Watcher interface {
 	StartBackgroundSync(ctx context.Context)
 	Finish()
@@ -24,70 +31,96 @@ type Watcher interface {
 
 // Handler handles all the watches for the git repositories. These watches are pulling the latest commit every syncPeriod.
 type Handler struct {
-	client      client.Client
-	watches     map[string]Watcher
-	createWatch func(gitRepo v1alpha1.GitRepo, client client.Client) Watcher // this func creates a watch. It's a struct field, so it can be replaced for a mock in unit tests.
-	log         logr.Logger
+	client    client.Client
+	log       logr.Logger
+	scheduler quartz.Scheduler
+	fetcher   GitFetcher
 }
 
-func NewHandler(client client.Client) *Handler {
+func NewHandler(ctx context.Context, client client.Client) *Handler {
+	scheduler := quartz.NewStdScheduler()
+	scheduler.Start(ctx)
 	return &Handler{
-		client:      client,
-		watches:     make(map[string]Watcher),
-		createWatch: NewWatch,
-		log:         ctrl.Log.WithName("git-latest-commit-poll-handler"),
+		client:    client,
+		log:       ctrl.Log.WithName("git-latest-commit-poll-handler"),
+		scheduler: scheduler,
+		fetcher:   &git.Fetch{},
 	}
 }
 
-// AddOrModifyGitRepoWatch adds a new watch for the gitrepo if no watch was already present.
-// It updates the existing watch for this gitrepo if present.
-func (h *Handler) AddOrModifyGitRepoWatch(ctx context.Context, gitRepo v1alpha1.GitRepo) {
-	key := getKey(gitRepo)
-	watch, found := h.watches[key]
-	if !found {
-		h.watches[key] = h.createWatch(gitRepo, h.client)
-		h.watches[key].StartBackgroundSync(ctx)
+// AddOrModifyGitRepoPollJob adds a new scheduled job for the gitrepo if no job was already present.
+// It updates the existing job for this gitrepo if present.
+func (h *Handler) AddOrModifyGitRepoPollJob(ctx context.Context, gitRepo v1alpha1.GitRepo) {
+	gitRepoPollKey := GitRepoPollKey(gitRepo)
+	scheduledJob, err := h.scheduler.GetScheduledJob(gitRepoPollKey)
+	if err != nil {
+		// job was not found
+		if gitRepo.Spec.DisablePolling {
+			// nothing to do if disablePolling is set
+			return
+		}
+		h.scheduleJob(ctx, gitRepoPollKey, gitRepo, true)
 	} else {
-		oldSyncInterval := watch.GetSyncInterval()
-		watch.UpdateGitRepo(gitRepo)
-
-		gitRepoSyncInterval := 0.0
-		if pi := gitRepo.Spec.PollingInterval; pi != nil {
-			gitRepoSyncInterval = pi.Seconds()
+		if gitRepo.Spec.DisablePolling {
+			// if polling is disabled, just delete the job from the scheduler
+			err = h.scheduler.DeleteJob(gitRepoPollKey)
+			if err != nil {
+				h.log.Error(err, "error deleting the job", "job", gitRepoPollKey)
+			}
+			return
 		}
-
-		if oldSyncInterval != gitRepoSyncInterval {
-			watch.Restart(ctx)
+		job := scheduledJob.JobDetail().Job()
+		gitRepoPollJob, ok := job.(*GitRepoPollJob)
+		if !ok {
+			h.log.Error(stderrors.New("invalid job"),
+				"error getting Gitrepo poll job, the scheduled job is not a GitRepoPollJob", "job", job.Description())
+			return
+		}
+		previousInterval := gitRepoPollJob.GitRepo.Spec.PollingInterval
+		gitRepoPollJob.GitRepo = gitRepo
+		if gitRepo.Spec.PollingInterval != previousInterval {
+			// ignoring the error because we're going to schedule immediately
+			_ = h.scheduler.DeleteJob(gitRepoPollKey)
+			h.scheduleJob(ctx, gitRepoPollKey, gitRepo, true)
 		}
 	}
 }
 
-// CleanUpWatches removes all watches whose gitrepo is not present in the cluster.
-func (h *Handler) CleanUpWatches(ctx context.Context) {
+// CleanUpGitRepoPollJobs removes all poll jobs whose gitrepo is not present in the cluster.
+func (h *Handler) CleanUpGitRepoPollJobs(ctx context.Context) {
 	var gitRepo v1alpha1.GitRepo
-	for key, watch := range h.watches {
-		namespacedName, err := getTypeNamespaceFromKey(key)
-		if err != nil {
-			h.log.Error(err, "can't get namespacedName", key)
+	for _, key := range h.scheduler.GetJobKeys() {
+		namespacedName := types.NamespacedName{
+			Namespace: key.Group(),
+			Name:      key.Name(),
 		}
-		if err = h.client.Get(ctx, namespacedName, &gitRepo); errors.IsNotFound(err) {
-			watch.Finish()
-			delete(h.watches, key)
+		if err := h.client.Get(ctx, namespacedName, &gitRepo); errors.IsNotFound(err) {
+			err = h.scheduler.DeleteJob(key)
+			if err != nil {
+				h.log.Error(err, "error deleting job", "job", key)
+			}
 		}
 	}
 }
 
-func getTypeNamespaceFromKey(key string) (types.NamespacedName, error) {
-	split := strings.Split(key, "-")
-	if len(split) < 2 {
-		return types.NamespacedName{}, fmt.Errorf("invalid key")
+func calculateSyncInterval(gitRepo v1alpha1.GitRepo) time.Duration {
+	if gitRepo.Spec.PollingInterval != nil {
+		return gitRepo.Spec.PollingInterval.Duration
 	}
-	return types.NamespacedName{
-		Namespace: split[1],
-		Name:      split[0],
-	}, nil
+
+	return defaultSyncInterval
 }
 
-func getKey(gitRepo v1alpha1.GitRepo) string {
-	return gitRepo.Name + "-" + gitRepo.Namespace
+func (h *Handler) scheduleJob(ctx context.Context, jobKey *quartz.JobKey, gitRepo v1alpha1.GitRepo, runBefore bool) {
+	job := NewGitRepoPollJob(h.client, h.fetcher, gitRepo)
+	if runBefore {
+		// ignoring error because that is only used by the quartz library to implement its retries mechanism
+		// The GitRepoPollJob always returns nil
+		_ = job.Execute(ctx)
+	}
+	err := h.scheduler.ScheduleJob(quartz.NewJobDetail(job, jobKey),
+		quartz.NewSimpleTrigger(calculateSyncInterval(gitRepo)))
+	if err != nil {
+		h.log.Error(err, "error scheduling job", "job", jobKey)
+	}
 }
diff --git a/pkg/git/poll/watch.go b/pkg/git/poll/watch.go
deleted file mode 100644
index 2a174cf9..00000000
--- a/pkg/git/poll/watch.go
+++ /dev/null
@@ -1,120 +0,0 @@
-package poll
-
-import (
-	"context"
-	"sync"
-	"time"
-
-	"github.com/go-logr/logr"
-	ctrl "sigs.k8s.io/controller-runtime"
-
-	"k8s.io/client-go/util/retry"
-
-	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
-	"github.com/rancher/fleet/pkg/git"
-	"k8s.io/apimachinery/pkg/types"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-)
-
-const defaultSyncInterval = 15
-
-type GitFetcher interface {
-	LatestCommit(ctx context.Context, gitrepo *v1alpha1.GitRepo, client client.Client) (string, error)
-}
-
-// Watch fetches the latest commit of a git repository referenced by a gitRepo with the syncInterval provided.
-type Watch struct {
-	gitRepo v1alpha1.GitRepo
-	client  client.Client
-	done    chan bool
-	mu      *sync.Mutex
-	fetcher GitFetcher
-	log     logr.Logger
-}
-
-func NewWatch(gitRepo v1alpha1.GitRepo, client client.Client) Watcher {
-	return &Watch{
-		gitRepo: gitRepo,
-		client:  client,
-		mu:      new(sync.Mutex),
-		fetcher: &git.Fetch{},
-		log:     ctrl.Log.WithName("git-latest-commit-poll-watch"),
-	}
-}
-
-// StartBackgroundSync fetches the latest commit every syncInternal in a goroutine.
-func (w *Watch) StartBackgroundSync(ctx context.Context) {
-	go w.fetchBySyncInterval(ctx, time.NewTicker(calculateSyncInterval(w.gitRepo)))
-}
-
-// Finish stops watching for changes in the git repo.
-func (w *Watch) Finish() {
-	w.done <- true
-}
-
-func (w *Watch) Restart(ctx context.Context) {
-	w.Finish()
-	w.StartBackgroundSync(ctx)
-}
-
-func (w *Watch) UpdateGitRepo(gitRepo v1alpha1.GitRepo) {
-	w.gitRepo = gitRepo
-}
-
-func (w *Watch) GetSyncInterval() float64 {
-	pi := w.gitRepo.Spec.PollingInterval
-	if pi == nil {
-		return 0
-	}
-
-	return pi.Seconds()
-}
-
-func (w *Watch) fetchBySyncInterval(ctx context.Context, ticker *time.Ticker) {
-	w.log.V(1).Info("start watching latest commit", "gitrepo-name", w.gitRepo.Name)
-	defer ticker.Stop()
-	w.done = make(chan bool)
-	w.fetchLatestCommitAndUpdateStatus(ctx)
-
-	for {
-		select {
-		case <-w.done:
-			return
-		case <-ticker.C:
-			w.mu.Lock()
-			w.fetchLatestCommitAndUpdateStatus(ctx)
-			w.mu.Unlock()
-		}
-	}
-}
-
-func (w *Watch) fetchLatestCommitAndUpdateStatus(ctx context.Context) {
-	commit, err := w.fetcher.LatestCommit(ctx, &w.gitRepo, w.client)
-	if err != nil {
-		w.log.Error(err, "error fetching commit", "gitrepo name", w.gitRepo.Name)
-		return
-	}
-	if w.gitRepo.Status.Commit != commit {
-		w.log.Info("new commit found", "gitrepo name", w.gitRepo.Name, "commit", commit)
-		if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
-			var gitRepoFromCluster v1alpha1.GitRepo
-			err := w.client.Get(ctx, types.NamespacedName{Name: w.gitRepo.Name, Namespace: w.gitRepo.Namespace}, &gitRepoFromCluster)
-			if err != nil {
-				return err
-			}
-			gitRepoFromCluster.Status.Commit = commit
-
-			return w.client.Status().Update(ctx, &gitRepoFromCluster)
-		}); err != nil {
-			w.log.Error(err, "error updating status when a new commit was found by polling", "gitrepo", w.gitRepo)
-		}
-	}
-}
-
-func calculateSyncInterval(gitRepo v1alpha1.GitRepo) time.Duration {
-	if gitRepo.Spec.PollingInterval != nil {
-		return gitRepo.Spec.PollingInterval.Duration
-	}
-
-	return time.Duration(defaultSyncInterval) * time.Second
-}


Test patch:
diff --git a/.github/workflows/e2e-ci.yml b/.github/workflows/e2e-ci.yml
index abd49759..2116ca8d 100644
--- a/.github/workflows/e2e-ci.yml
+++ b/.github/workflows/e2e-ci.yml
@@ -71,7 +71,7 @@ jobs:
       -
         name: Import Images Into k3d
         run: |
-          ./.github/scripts/k3d-import-retry.sh rancher/fleet:dev rancher/fleet-agent:dev rancher/gitjob:dev nginx-git:test
+          ./.github/scripts/k3d-import-retry.sh rancher/fleet:dev rancher/fleet-agent:dev nginx-git:test
       -
         name: Set Up Tmate Debug Session
         if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.enable_tmate == 'true' }}
diff --git a/.github/workflows/e2e-multicluster-ci.yml b/.github/workflows/e2e-multicluster-ci.yml
index a3bb8908..01a6baaa 100644
--- a/.github/workflows/e2e-multicluster-ci.yml
+++ b/.github/workflows/e2e-multicluster-ci.yml
@@ -72,7 +72,7 @@ jobs:
       -
         name: Import Images Into k3d
         run: |
-          ./.github/scripts/k3d-import-retry.sh rancher/fleet:dev rancher/fleet-agent:dev rancher/gitjob:dev -c upstream
+          ./.github/scripts/k3d-import-retry.sh rancher/fleet:dev rancher/fleet-agent:dev -c upstream
           ./.github/scripts/k3d-import-retry.sh rancher/fleet-agent:dev -c downstream
       -
         name: Set Up Tmate Debug Session
diff --git a/e2e/single-cluster/gitrepo_test.go b/e2e/single-cluster/gitrepo_test.go
index 7aa38dae..400251d5 100644
--- a/e2e/single-cluster/gitrepo_test.go
+++ b/e2e/single-cluster/gitrepo_test.go
@@ -5,19 +5,24 @@ package singlecluster_test
 
 import (
 	"bytes"
+	"encoding/json"
 	"fmt"
 	"math/rand"
 	"os"
 	"path"
+	"reflect"
 	"strings"
 
 	"github.com/go-git/go-git/v5"
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
+	"github.com/onsi/gomega/types"
 
 	"github.com/rancher/fleet/e2e/testenv"
 	"github.com/rancher/fleet/e2e/testenv/githelper"
 	"github.com/rancher/fleet/e2e/testenv/kubectl"
+	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+	"github.com/rancher/wrangler/v2/pkg/genericcondition"
 )
 
 const (
@@ -105,11 +110,60 @@ var _ = Describe("Monitoring Git repos via HTTP for change", Label("infra-setup"
 			commit, err := gh.Update(clone)
 			Expect(err).ToNot(HaveOccurred())
 
-			By("checking for the updated commit hash in gitrepo")
-			Eventually(func() string {
-				out, _ := k.Get("gitrepo", gitrepoName, "-o", "yaml")
-				return out
-			}).Should(ContainSubstring("commit: " + commit))
+			By("updating the gitrepo's status")
+			expectedStatus := fleet.GitRepoStatus{
+				Commit:               commit,
+				ReadyClusters:        1,
+				DesiredReadyClusters: 1,
+				GitJobStatus:         "Current",
+				Summary: fleet.BundleSummary{
+					NotReady:          0,
+					WaitApplied:       0,
+					ErrApplied:        0,
+					OutOfSync:         0,
+					Modified:          0,
+					Ready:             1,
+					Pending:           0,
+					DesiredReady:      1,
+					NonReadyResources: []fleet.NonReadyResource(nil),
+				},
+				Display: fleet.GitRepoDisplay{
+					ReadyBundleDeployments: "1/1",
+					// XXX: add state and message?
+				},
+				Conditions: []genericcondition.GenericCondition{
+					{
+						Type:   "Ready",
+						Status: "True",
+					},
+					{
+						Type:   "Accepted",
+						Status: "True",
+					},
+					{
+						Type:   "Reconciling",
+						Status: "False",
+					},
+					{
+						Type:   "Stalled",
+						Status: "False",
+					},
+				},
+				ResourceCounts: fleet.GitRepoResourceCounts{
+					Ready:        1,
+					DesiredReady: 1,
+					WaitApplied:  0,
+					Modified:     0,
+					Orphaned:     0,
+					Missing:      0,
+					Unknown:      0,
+					NotReady:     0,
+				},
+			}
+			Eventually(func(g Gomega) {
+				status := getGitRepoStatus(k, gitrepoName)
+				g.Expect(status).To(matchGitRepoStatus(expectedStatus))
+			}).Should(Succeed())
 
 			By("checking the deployment's new name")
 			Eventually(func() string {
@@ -122,6 +176,7 @@ var _ = Describe("Monitoring Git repos via HTTP for change", Label("infra-setup"
 	When("updating a git repository monitored via webhook", func() {
 		BeforeEach(func() {
 			repoName = "webhook-test"
+			targetNamespace = testenv.NewNamespaceName("target", r)
 		})
 
 		JustBeforeEach(func() {
@@ -213,11 +268,61 @@ var _ = Describe("Monitoring Git repos via HTTP for change", Label("infra-setup"
 			commit, err := gh.Update(clone)
 			Expect(err).ToNot(HaveOccurred())
 
-			By("checking for the updated commit hash in gitrepo")
-			Eventually(func() string {
-				out, _ := k.Get("gitrepo", gitrepoName, "-o", "yaml")
-				return out
-			}).Should(ContainSubstring("commit: " + commit))
+			By("updating the gitrepo's status")
+			expectedStatus := fleet.GitRepoStatus{
+				Commit:               commit,
+				ReadyClusters:        1,
+				DesiredReadyClusters: 1,
+				GitJobStatus:         "Current",
+				Summary: fleet.BundleSummary{
+					NotReady:          0,
+					WaitApplied:       0,
+					ErrApplied:        0,
+					OutOfSync:         0,
+					Modified:          0,
+					Ready:             1,
+					Pending:           0,
+					DesiredReady:      1,
+					NonReadyResources: []fleet.NonReadyResource(nil),
+				},
+				Display: fleet.GitRepoDisplay{
+					ReadyBundleDeployments: "1/1",
+					// XXX: add state and message?
+				},
+				Conditions: []genericcondition.GenericCondition{
+					{
+						Type:   "Ready",
+						Status: "True",
+					},
+					{
+						Type:   "Accepted",
+						Status: "True",
+					},
+					{
+						Type:   "Reconciling",
+						Status: "False",
+					},
+					{
+						Type:   "Stalled",
+						Status: "False",
+					},
+				},
+				ResourceCounts: fleet.GitRepoResourceCounts{
+					Ready:        1,
+					DesiredReady: 1,
+					WaitApplied:  0,
+					Modified:     0,
+					Orphaned:     0,
+					Missing:      0,
+					Unknown:      0,
+					NotReady:     0,
+				},
+			}
+			Eventually(func(g Gomega) {
+				status := getGitRepoStatus(k, gitrepoName)
+				g.Expect(status).To(matchGitRepoStatus(expectedStatus))
+
+			}).Should(Succeed())
 
 			By("checking the deployment's new name")
 			Eventually(func() string {
@@ -238,3 +343,70 @@ func replace(path string, s string, r string) {
 	err = os.WriteFile(path, b, 0644)
 	Expect(err).ToNot(HaveOccurred())
 }
+
+// getGitRepoStatus retrieves the status of the gitrepo with the provided name.
+func getGitRepoStatus(k kubectl.Command, name string) fleet.GitRepoStatus {
+	gr, err := k.Get("gitrepo", name, "-o=json")
+
+	Expect(err).ToNot(HaveOccurred())
+
+	var gitrepo fleet.GitRepo
+	_ = json.Unmarshal([]byte(gr), &gitrepo)
+
+	return gitrepo.Status
+}
+
+type gitRepoStatusMatcher struct {
+	expected fleet.GitRepoStatus
+}
+
+func matchGitRepoStatus(expected fleet.GitRepoStatus) types.GomegaMatcher {
+	return &gitRepoStatusMatcher{expected: expected}
+}
+
+func (matcher *gitRepoStatusMatcher) Match(actual interface{}) (success bool, err error) {
+	got, ok := actual.(fleet.GitRepoStatus)
+	if !ok {
+		return false, fmt.Errorf("gitRepoStatusMatcher expects a GitRepoStatus")
+	}
+
+	want := matcher.expected
+
+	// Conditions are tested using custom logic to avoid having to manipulate timestamps (last update and transition
+	// times).
+	for _, wantCond := range want.Conditions {
+		found := false
+		for _, gotCond := range got.Conditions {
+			if gotCond.Type == wantCond.Type &&
+				wantCond.Status == gotCond.Status &&
+				wantCond.Reason == gotCond.Reason &&
+				wantCond.Message == gotCond.Message {
+				found = true
+			}
+		}
+		if !found {
+			return false, fmt.Errorf(
+				"Condition %q with status %q not found",
+				wantCond.Type,
+				wantCond.Status,
+			)
+		}
+	}
+
+	return got.Commit == want.Commit &&
+			got.ReadyClusters == want.ReadyClusters &&
+			got.DesiredReadyClusters == want.DesiredReadyClusters &&
+			got.GitJobStatus == want.GitJobStatus &&
+			reflect.DeepEqual(got.Summary, want.Summary) &&
+			got.Display.ReadyBundleDeployments == want.Display.ReadyBundleDeployments &&
+			got.ResourceCounts == want.ResourceCounts,
+		nil
+}
+
+func (matcher *gitRepoStatusMatcher) FailureMessage(actual interface{}) (message string) {
+	return fmt.Sprintf("Expected\n\t%#v\nto match status\n\t%#v", actual, matcher.expected)
+}
+
+func (matcher *gitRepoStatusMatcher) NegatedFailureMessage(actual interface{}) (message string) {
+	return fmt.Sprintf("Expected\n\t%#v\nnot to match status\n\t%#v", actual, matcher.expected)
+}
diff --git a/integrationtests/controller/gitrepo/gitrepo_test.go b/integrationtests/controller/gitrepo/gitrepo_test.go
index 0b8ddc04..707ca001 100644
--- a/integrationtests/controller/gitrepo/gitrepo_test.go
+++ b/integrationtests/controller/gitrepo/gitrepo_test.go
@@ -80,9 +80,11 @@ var _ = Describe("GitRepo", Ordered, func() {
 			Expect(gitrepo.Status.Display.ReadyBundleDeployments).To(Equal("0/0"))
 			Expect(gitrepo.Status.Display.State).To(Equal("GitUpdating"))
 			Expect(gitrepo.Status.Display.Error).To(BeFalse())
-			Expect(gitrepo.Status.Conditions).To(HaveLen(1))
-			Expect(gitrepo.Status.Conditions[0].Type).To(Equal("Accepted"))
+			Expect(gitrepo.Status.Conditions).To(HaveLen(2))
+			Expect(gitrepo.Status.Conditions[0].Type).To(Equal("Ready"))
 			Expect(string(gitrepo.Status.Conditions[0].Status)).To(Equal("True"))
+			Expect(gitrepo.Status.Conditions[1].Type).To(Equal("Accepted"))
+			Expect(string(gitrepo.Status.Conditions[1].Status)).To(Equal("True"))
 			Expect(gitrepo.Status.DeepCopy().ObservedGeneration).To(Equal(int64(1)))
 		})
 	})
diff --git a/integrationtests/gitcloner/clone_test.go b/integrationtests/gitcloner/clone_test.go
index 6ab5494a..a3ee4fcc 100644
--- a/integrationtests/gitcloner/clone_test.go
+++ b/integrationtests/gitcloner/clone_test.go
@@ -17,6 +17,8 @@ import (
 	"github.com/rancher/fleet/cmd/gitcloner/cmd"
 	"github.com/rancher/fleet/cmd/gitcloner/gogit"
 
+	dockercontainer "github.com/docker/docker/api/types/container"
+	dockermount "github.com/docker/docker/api/types/mount"
 	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/config"
 	"github.com/go-git/go-git/v5/plumbing/object"
@@ -286,11 +288,15 @@ func createGogsContainerWithHTTPS() (testcontainers.Container, error) {
 	req := testcontainers.ContainerRequest{
 		Image:        "gogs/gogs:0.13",
 		ExposedPorts: []string{gogsHTTPSPort + "/tcp", gogsSSHPort + "/tcp"},
-		Mounts: testcontainers.ContainerMounts{
-			{
-				Source: testcontainers.GenericBindMountSource{HostPath: tmpDir},
-				Target: "/data",
-			},
+		HostConfigModifier: func(hostConfig *dockercontainer.HostConfig) {
+			hostConfig.Mounts = []dockermount.Mount{
+				{
+					Type:   dockermount.TypeBind,
+					Source: tmpDir,
+					Target: "/data",
+				},
+			}
+
 		},
 	}
 	container, err := testcontainers.GenericContainer(context.Background(), testcontainers.GenericContainerRequest{
diff --git a/integrationtests/gitjob/controller/suite_test.go b/integrationtests/gitjob/controller/suite_test.go
index 68624474..d7ece82a 100644
--- a/integrationtests/gitjob/controller/suite_test.go
+++ b/integrationtests/gitjob/controller/suite_test.go
@@ -68,8 +68,8 @@ var _ = BeforeSuite(func() {
 	gitPollerMock := mocks.NewMockGitPoller(ctlr)
 
 	// do nothing if gitPoller is called. gitPoller calls are tested in unit tests
-	gitPollerMock.EXPECT().AddOrModifyGitRepoWatch(gomock.Any(), gomock.Any()).AnyTimes()
-	gitPollerMock.EXPECT().CleanUpWatches(gomock.Any()).AnyTimes()
+	gitPollerMock.EXPECT().AddOrModifyGitRepoPollJob(gomock.Any(), gomock.Any()).AnyTimes()
+	gitPollerMock.EXPECT().CleanUpGitRepoPollJobs(gomock.Any()).AnyTimes()
 
 	err = (&gitjob.GitJobReconciler{
 		Client:    mgr.GetClient(),
diff --git a/integrationtests/gitjob/git/git_test.go b/integrationtests/gitjob/git/git_test.go
index da01d743..d95fa61f 100644
--- a/integrationtests/gitjob/git/git_test.go
+++ b/integrationtests/gitjob/git/git_test.go
@@ -13,6 +13,8 @@ import (
 	"testing"
 	"time"
 
+	dockercontainer "github.com/docker/docker/api/types/container"
+	dockermount "github.com/docker/docker/api/types/mount"
 	gogit "github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/config"
 	"github.com/go-git/go-git/v5/plumbing/object"
@@ -286,11 +288,15 @@ func createGogsContainer(ctx context.Context, tmpDir string) (testcontainers.Con
 			wait.ForHTTP("/").WithPort("3000/tcp").WithStartupTimeout(startupTimeout),
 			wait.ForListeningPort("22/tcp").WithStartupTimeout(startupTimeout),
 		),
-		Mounts: testcontainers.ContainerMounts{
-			{
-				Source: testcontainers.GenericBindMountSource{HostPath: tmpDir},
-				Target: "/data",
-			},
+		HostConfigModifier: func(hostConfig *dockercontainer.HostConfig) {
+			hostConfig.Mounts = []dockermount.Mount{
+				{
+					Type:   dockermount.TypeBind,
+					Source: tmpDir,
+					Target: "/data",
+				},
+			}
+
 		},
 	}
 	container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
diff --git a/internal/cmd/gitjob/gitjob_test.go b/internal/cmd/gitjob/gitjob_test.go
index 566aca98..cfd0f2ca 100644
--- a/internal/cmd/gitjob/gitjob_test.go
+++ b/internal/cmd/gitjob/gitjob_test.go
@@ -27,7 +27,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 )
 
-func TestReconcile_AddOrModifyGitRepoWatchIsCalled_WhenGitRepoIsCreatedOrModified(t *testing.T) {
+func TestReconcile_AddOrModifyGitRepoPollJobIsCalled_WhenGitRepoIsCreatedOrModified(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 	defer mockCtrl.Finish()
 	scheme := runtime.NewScheme()
@@ -46,8 +46,8 @@ func TestReconcile_AddOrModifyGitRepoWatchIsCalled_WhenGitRepoIsCreatedOrModifie
 	client.EXPECT().Get(ctx, gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
 	client.EXPECT().Status().Return(statusClient)
 	poller := mocks.NewMockGitPoller(mockCtrl)
-	poller.EXPECT().AddOrModifyGitRepoWatch(ctx, gomock.Any()).Times(1)
-	poller.EXPECT().CleanUpWatches(ctx).Times(0)
+	poller.EXPECT().AddOrModifyGitRepoPollJob(ctx, gomock.Any()).Times(1)
+	poller.EXPECT().CleanUpGitRepoPollJobs(ctx).Times(0)
 
 	r := GitJobReconciler{
 		Client:    client,
@@ -71,8 +71,8 @@ func TestReconcile_PurgeWatchesIsCalled_WhenGitRepoIsCreatedOrModified(t *testin
 	client := mocks.NewMockClient(mockCtrl)
 	client.EXPECT().Get(ctx, namespacedName, gomock.Any()).Times(1).Return(errors.NewNotFound(schema.GroupResource{}, "NotFound"))
 	poller := mocks.NewMockGitPoller(mockCtrl)
-	poller.EXPECT().AddOrModifyGitRepoWatch(ctx, gomock.Any()).Times(0)
-	poller.EXPECT().CleanUpWatches(ctx).Times(1)
+	poller.EXPECT().AddOrModifyGitRepoPollJob(ctx, gomock.Any()).Times(0)
+	poller.EXPECT().CleanUpGitRepoPollJobs(ctx).Times(1)
 
 	r := GitJobReconciler{
 		Client:    client,
@@ -103,8 +103,8 @@ func TestNewJob(t *testing.T) { // nolint:funlen
 	utilruntime.Must(batchv1.AddToScheme(scheme))
 	ctx := context.TODO()
 	poller := mocks.NewMockGitPoller(mockCtrl)
-	poller.EXPECT().AddOrModifyGitRepoWatch(ctx, gomock.Any()).AnyTimes()
-	poller.EXPECT().CleanUpWatches(ctx).AnyTimes()
+	poller.EXPECT().AddOrModifyGitRepoPollJob(ctx, gomock.Any()).AnyTimes()
+	poller.EXPECT().CleanUpGitRepoPollJobs(ctx).AnyTimes()
 
 	tests := map[string]struct {
 		gitrepo                *fleetv1.GitRepo
@@ -519,8 +519,8 @@ func TestGenerateJob_EnvVars(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 	ctx := context.TODO()
 	poller := mocks.NewMockGitPoller(mockCtrl)
-	poller.EXPECT().AddOrModifyGitRepoWatch(ctx, gomock.Any()).AnyTimes()
-	poller.EXPECT().CleanUpWatches(ctx).AnyTimes()
+	poller.EXPECT().AddOrModifyGitRepoPollJob(ctx, gomock.Any()).AnyTimes()
+	poller.EXPECT().CleanUpGitRepoPollJobs(ctx).AnyTimes()
 
 	tests := map[string]struct {
 		gitrepo                      *fleetv1.GitRepo
diff --git a/pkg/git/poll/gitrepopolljob_test.go b/pkg/git/poll/gitrepopolljob_test.go
new file mode 100644
index 00000000..bbf200fa
--- /dev/null
+++ b/pkg/git/poll/gitrepopolljob_test.go
@@ -0,0 +1,91 @@
+package poll
+
+import (
+	"context"
+	"fmt"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"go.uber.org/mock/gomock"
+	"golang.org/x/sync/semaphore"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	// . "github.com/onsi/gomega"
+	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+	"github.com/rancher/fleet/pkg/git/mocks"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+)
+
+var _ = Describe("GitRepoPollJob tests", func() {
+	var (
+		expectedCalls func(fetcher *mocks.MockGitFetcher)
+		gitRepo       v1alpha1.GitRepo
+		job           GitRepoPollJob
+		fetcher       *mocks.MockGitFetcher
+		client        client.WithWatch
+		ctx           context.Context
+		commit        string
+	)
+
+	JustBeforeEach(func() {
+		ctrl := gomock.NewController(GinkgoT())
+
+		gitRepo = v1alpha1.GitRepo{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "gitrepo",
+			},
+		}
+		fetcher = mocks.NewMockGitFetcher(ctrl)
+		scheme := runtime.NewScheme()
+		err := v1alpha1.AddToScheme(scheme)
+		Expect(err).ToNot(HaveOccurred())
+		client = fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(&gitRepo).WithStatusSubresource(&gitRepo).Build()
+		ctx = context.TODO()
+
+		job = GitRepoPollJob{
+			sem:     semaphore.NewWeighted(1),
+			client:  client,
+			GitRepo: gitRepo,
+			fetcher: fetcher,
+		}
+
+		expectedCalls(fetcher)
+
+		err = job.Execute(ctx)
+		Expect(err).ToNot(HaveOccurred())
+	})
+
+	When("Running the job with a commit different to the one set in the gitRepo", func() {
+		BeforeEach(func() {
+			commit = "fakeCommit"
+			expectedCalls = func(fetcher *mocks.MockGitFetcher) {
+				fetcher.EXPECT().LatestCommit(ctx, gomock.Any(), client).Return(commit, nil).Times(1)
+			}
+			gitRepo.Status.Commit = "9b0380f535d4c428d5b18f2efb5fddfe52b9dbf1"
+		})
+		It("Should update the gitRepo commit", func() {
+			updatedGitRepo := v1alpha1.GitRepo{}
+			err := client.Get(ctx, types.NamespacedName{Name: gitRepo.Name, Namespace: gitRepo.Namespace}, &updatedGitRepo)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(updatedGitRepo.Status.Commit).To(Equal(commit))
+		})
+	})
+	When("Running the job and LatestCommit returns an error", func() {
+		BeforeEach(func() {
+			commit = "fakeCommit"
+			expectedCalls = func(fetcher *mocks.MockGitFetcher) {
+				fetcher.EXPECT().LatestCommit(ctx, gomock.Any(), client).Return("", fmt.Errorf("Some error")).Times(1)
+			}
+			gitRepo.Status.Commit = "9b0380f535d4c428d5b18f2efb5fddfe52b9dbf1"
+		})
+		It("Should not update the original gitRepo commit", func() {
+			updatedGitRepo := v1alpha1.GitRepo{}
+			err := client.Get(ctx, types.NamespacedName{Name: gitRepo.Name, Namespace: gitRepo.Namespace}, &updatedGitRepo)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(updatedGitRepo.Status.Commit).To(Equal(gitRepo.Status.Commit))
+		})
+	})
+})
diff --git a/pkg/git/poll/handler_test.go b/pkg/git/poll/handler_test.go
index 5efc3361..d7c73a87 100644
--- a/pkg/git/poll/handler_test.go
+++ b/pkg/git/poll/handler_test.go
@@ -1,91 +1,280 @@
-//go:generate mockgen --build_flags=--mod=mod -destination=../mocks/watch_mock.go -package=mocks github.com/rancher/fleet/pkg/git/poll Watcher
 package poll
 
 import (
 	"context"
-	"testing"
 	"time"
 
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"github.com/reugn/go-quartz/quartz"
+	gomock "go.uber.org/mock/gomock"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+
 	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 	"github.com/rancher/fleet/pkg/git/mocks"
-
-	"github.com/google/go-cmp/cmp"
-	"go.uber.org/mock/gomock"
-	"golang.org/x/exp/maps"
-
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"sigs.k8s.io/controller-runtime/pkg/client"
+	"k8s.io/apimachinery/pkg/runtime"
 )
 
-func TestAddOrModifyWatchGitRepo(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-
-	ctx := context.TODO()
-	gitRepo := v1alpha1.GitRepo{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      "gitjob",
-			Namespace: "test",
-		},
-	}
-
-	tests := map[string]struct {
-		watches         func(mockWatcher *mocks.MockWatcher) map[string]Watcher
-		pollingInterval time.Duration
-		expectedWatches []string
-		expectedCalls   func(mockWatcher *mocks.MockWatcher)
-	}{
-		"gitrepo not present": {
-			watches: func(mockWatcher *mocks.MockWatcher) map[string]Watcher {
-				return make(map[string]Watcher)
-			},
-			expectedWatches: []string{"gitjob-test"},
-			expectedCalls: func(mockWatcher *mocks.MockWatcher) {
-				mockWatcher.EXPECT().StartBackgroundSync(ctx).Times(1)
+var _ = Describe("Gitrepo pooling tests", func() {
+	var (
+		scheduler *mocks.MockScheduler
+		fetcher   *mocks.MockGitFetcher
+		gitRepo   v1alpha1.GitRepo
+		client    client.Client
+	)
+	BeforeEach(func() {
+		ctrl := gomock.NewController(GinkgoT())
+		scheduler = mocks.NewMockScheduler(ctrl)
+		fetcher = mocks.NewMockGitFetcher(ctrl)
+		gitRepo = v1alpha1.GitRepo{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "gitjob",
+				Namespace: "test",
 			},
+		}
+		scheme := runtime.NewScheme()
+		err := v1alpha1.AddToScheme(scheme)
+		Expect(err).ToNot(HaveOccurred())
+		client = fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(&gitRepo).WithStatusSubresource(&gitRepo).Build()
+	})
+	DescribeTable("Gitrepo pooling tests",
+		func(pollingInterval time.Duration, disablePolling bool,
+			schedulerCalls func(gitRepo v1alpha1.GitRepo, pollingInterval time.Duration),
+			fetcherCalls func()) {
+			if pollingInterval != 0 {
+				gitRepo.Spec.PollingInterval = &metav1.Duration{Duration: pollingInterval}
+			} else {
+				gitRepo.Spec.PollingInterval = nil
+			}
+			gitRepo.Spec.DisablePolling = disablePolling
+
+			handler := Handler{
+				scheduler: scheduler,
+				fetcher:   fetcher,
+				client:    client,
+			}
+
+			schedulerCalls(gitRepo, pollingInterval)
+			fetcherCalls()
+
+			handler.AddOrModifyGitRepoPollJob(context.TODO(), gitRepo)
 		},
-		"gitrepo present with same pollingInterval": {
-			watches: func(mockWatcher *mocks.MockWatcher) map[string]Watcher {
-				return map[string]Watcher{"gitjob-test": mockWatcher}
-			},
-			pollingInterval: 10 * time.Second,
-			expectedWatches: []string{"gitjob-test"},
-			expectedCalls: func(mockWatcher *mocks.MockWatcher) {
-				mockWatcher.EXPECT().GetSyncInterval().Return(10.0).Times(1)
-				mockWatcher.EXPECT().UpdateGitRepo(gitRepo)
+
+		Entry("GitRepo is not present and disablePolling = true",
+			1*time.Second, true, func(gitRepo v1alpha1.GitRepo, pollingInterval time.Duration) {
+				key := GitRepoPollKey(gitRepo)
+				scheduler.EXPECT().GetScheduledJob(key).Return(nil, quartz.ErrJobNotFound).Times(1)
+			}, func() {}),
+
+		Entry("Gitrepo is not present, not setting pollingInterval and disablePolling=false",
+			0*time.Second, false, func(gitRepo v1alpha1.GitRepo, pollingInterval time.Duration) {
+				key := GitRepoPollKey(gitRepo)
+				scheduler.EXPECT().GetScheduledJob(key).Return(nil, quartz.ErrJobNotFound).Times(1)
+
+				job := NewGitRepoPollJob(client, fetcher, gitRepo)
+				jobDetail := quartz.NewJobDetail(job, key)
+				// we're not specifying an internal, so the default (15 secs) is set
+				trigger := quartz.NewSimpleTrigger(15 * time.Second)
+				scheduler.EXPECT().ScheduleJob(jobDetail, trigger).Return(nil).Times(1)
+			}, func() {
+				fetcher.EXPECT().LatestCommit(gomock.Any(), gomock.Any(), gomock.Any()).Return("commit", nil).Times(1)
 			},
-		},
-		"gitrepo present with different pollingInterval": {
-			watches: func(mockWatcher *mocks.MockWatcher) map[string]Watcher {
-				return map[string]Watcher{"gitjob-test": mockWatcher}
+		),
+
+		Entry("Gitrepo is not present, setting pollingInterval to a specific value and disablePolling=false",
+			1999*time.Second, false, func(gitRepo v1alpha1.GitRepo, pollingInterval time.Duration) {
+				key := GitRepoPollKey(gitRepo)
+				scheduler.EXPECT().GetScheduledJob(key).Return(nil, quartz.ErrJobNotFound).Times(1)
+
+				job := NewGitRepoPollJob(client, fetcher, gitRepo)
+				jobDetail := quartz.NewJobDetail(job, key)
+				// trigger should be pollingInterval seconds
+				trigger := quartz.NewSimpleTrigger(pollingInterval)
+				scheduler.EXPECT().ScheduleJob(jobDetail, trigger).Return(nil).Times(1)
+			}, func() {
+				fetcher.EXPECT().LatestCommit(gomock.Any(), gomock.Any(), gomock.Any()).Return("commit", nil).Times(1)
 			},
-			pollingInterval: 1 * time.Second,
-			expectedWatches: []string{"gitjob-test"},
-			expectedCalls: func(mockWatcher *mocks.MockWatcher) {
-				mockWatcher.EXPECT().GetSyncInterval().Return(10.0).Times(1)
-				mockWatcher.EXPECT().UpdateGitRepo(gitRepo)
-				mockWatcher.EXPECT().Restart(ctx)
+		),
+
+		Entry("gitrepo present, same polling interval, disablePolling=true",
+			10*time.Second, true, func(gitRepo v1alpha1.GitRepo, pollingInterval time.Duration) {
+				key := GitRepoPollKey(gitRepo)
+				job := NewGitRepoPollJob(client, fetcher, gitRepo)
+				jobDetail := quartz.NewJobDetail(job, key)
+				schedJob := &mocks.MockScheduledJob{
+					Detail:          jobDetail,
+					TriggerDuration: 10 * time.Second,
+				}
+				// job exists
+				scheduler.EXPECT().GetScheduledJob(key).Return(schedJob, nil).Times(1)
+				// but we delete it because disablePolling is true
+				scheduler.EXPECT().DeleteJob(key).Return(nil).Times(1)
+			}, func() {},
+		),
+
+		Entry("gitrepo present, different polling interval, disablePolling=true",
+			10*time.Second, true, func(gitRepo v1alpha1.GitRepo, pollingInterval time.Duration) {
+				key := GitRepoPollKey(gitRepo)
+				job := NewGitRepoPollJob(client, fetcher, gitRepo)
+				jobDetail := quartz.NewJobDetail(job, key)
+				schedJob := &mocks.MockScheduledJob{
+					Detail:          jobDetail,
+					TriggerDuration: 10 * time.Second,
+				}
+				// job exists with a polling interval of 10 seconds
+				scheduler.EXPECT().GetScheduledJob(key).Return(schedJob, nil).Times(1)
+				// we delete it because disablePolling is true
+				scheduler.EXPECT().DeleteJob(key).Return(nil).Times(1)
+			}, func() {},
+		),
+
+		Entry("gitrepo present, same polling interval, disablePolling=false",
+			10*time.Second, false, func(gitRepo v1alpha1.GitRepo, pollingInterval time.Duration) {
+				key := GitRepoPollKey(gitRepo)
+				job := NewGitRepoPollJob(client, fetcher, gitRepo)
+				jobDetail := quartz.NewJobDetail(job, key)
+				schedJob := &mocks.MockScheduledJob{
+					Detail:          jobDetail,
+					TriggerDuration: 10 * time.Second,
+				}
+				// gets the job and does nothing else
+				scheduler.EXPECT().GetScheduledJob(key).Return(schedJob, nil).Times(1)
+			}, func() {},
+		),
+
+		Entry("gitrepo present, different polling interval, disablePolling=false",
+			1999*time.Second, false, func(gitRepo v1alpha1.GitRepo, pollingInterval time.Duration) {
+				gitRepoCopy := gitRepo
+				gitRepoCopy.Spec.PollingInterval = &metav1.Duration{Duration: 10 * time.Second}
+				key := GitRepoPollKey(gitRepoCopy)
+				job := NewGitRepoPollJob(client, fetcher, gitRepoCopy)
+				jobDetail := quartz.NewJobDetail(job, key)
+				schedJob := &mocks.MockScheduledJob{
+					Detail:          jobDetail,
+					TriggerDuration: 10 * time.Second,
+				}
+				// gets the job and does nothing else
+				scheduler.EXPECT().GetScheduledJob(key).Return(schedJob, nil).Times(1)
+				scheduler.EXPECT().DeleteJob(key).Return(nil).Times(1)
+				trigger := quartz.NewSimpleTrigger(1999 * time.Second)
+				scheduler.EXPECT().ScheduleJob(jobDetail, trigger).Return(nil).Times(1)
+			}, func() {
+				fetcher.EXPECT().LatestCommit(gomock.Any(), gomock.Any(), gomock.Any()).Return("commit", nil).Times(1)
 			},
-		},
-	}
-
-	for name, test := range tests {
-		t.Run(name, func(t *testing.T) {
-			watcher := mocks.NewMockWatcher(ctrl)
-			h := Handler{
-				watches: test.watches(watcher),
-				createWatch: func(_ v1alpha1.GitRepo, _ client.Client) Watcher {
-					return watcher
+		),
+	)
+})
+
+var _ = Describe("Gitrepo cleanup tests", func() {
+	var (
+		scheduler              *mocks.MockScheduler
+		fetcher                *mocks.MockGitFetcher
+		gitRepo                v1alpha1.GitRepo
+		client                 client.Client
+		handler                Handler
+		expectedSchedulerCalls func(sched *mocks.MockScheduler)
+	)
+	JustBeforeEach(func() {
+		ctrl := gomock.NewController(GinkgoT())
+		scheduler = mocks.NewMockScheduler(ctrl)
+		fetcher = mocks.NewMockGitFetcher(ctrl)
+		handler = Handler{
+			scheduler: scheduler,
+			fetcher:   fetcher,
+			client:    client,
+		}
+	})
+	When("All gitRepos are found", func() {
+		BeforeEach(func() {
+			scheme := runtime.NewScheme()
+			err := v1alpha1.AddToScheme(scheme)
+			Expect(err).ToNot(HaveOccurred())
+			gitRepo1 := v1alpha1.GitRepo{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "gitjob1",
+					Namespace: "test",
+				},
+			}
+
+			gitRepo2 := v1alpha1.GitRepo{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "gitjob2",
+					Namespace: "test",
+				},
+			}
+
+			gitRepo3 := v1alpha1.GitRepo{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "gitjob3",
+					Namespace: "test",
+				},
+			}
+			client = fake.NewClientBuilder().
+				WithScheme(scheme).
+				WithRuntimeObjects(&gitRepo1, &gitRepo2, &gitRepo3).
+				WithStatusSubresource(&gitRepo, &gitRepo2, &gitRepo3).
+				Build()
+
+			expectedSchedulerCalls = func(sched *mocks.MockScheduler) {
+				key1 := GitRepoPollKey(gitRepo1)
+				key2 := GitRepoPollKey(gitRepo2)
+				key3 := GitRepoPollKey(gitRepo3)
+				sched.EXPECT().GetJobKeys().Return([]*quartz.JobKey{key1, key2, key3}).Times(1)
+			}
+
+		})
+		It("Does nothing", func() {
+			expectedSchedulerCalls(scheduler)
+			ctx := context.TODO()
+			handler.CleanUpGitRepoPollJobs(ctx)
+		})
+	})
+	When("A gitRepo is not found", func() {
+		BeforeEach(func() {
+			scheme := runtime.NewScheme()
+			err := v1alpha1.AddToScheme(scheme)
+			Expect(err).ToNot(HaveOccurred())
+			gitRepo1 := v1alpha1.GitRepo{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "gitjob1",
+					Namespace: "test",
 				},
 			}
-			gitRepo.Spec.PollingInterval = &metav1.Duration{Duration: test.pollingInterval}
 
-			test.expectedCalls(watcher)
-			h.AddOrModifyGitRepoWatch(ctx, gitRepo)
+			gitRepo2 := v1alpha1.GitRepo{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "gitjob2",
+					Namespace: "test",
+				},
+			}
 
-			if !cmp.Equal(maps.Keys(h.watches), test.expectedWatches) {
-				t.Errorf("expected %v, but got %v", test.expectedWatches, maps.Keys(h.watches))
+			gitRepo3 := v1alpha1.GitRepo{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "gitjob3",
+					Namespace: "test",
+				},
 			}
+			client = fake.NewClientBuilder().
+				WithScheme(scheme).
+				WithRuntimeObjects(&gitRepo1, &gitRepo2).
+				WithStatusSubresource(&gitRepo, &gitRepo2).
+				Build()
+
+			expectedSchedulerCalls = func(sched *mocks.MockScheduler) {
+				key1 := GitRepoPollKey(gitRepo1)
+				key2 := GitRepoPollKey(gitRepo2)
+				// gitRepo3 was not added to the client
+				key3 := GitRepoPollKey(gitRepo3)
+				sched.EXPECT().GetJobKeys().Return([]*quartz.JobKey{key1, key2, key3}).Times(1)
+				sched.EXPECT().DeleteJob(key3).Return(nil).Times(1)
+			}
+		})
+		It("Deletes the job that was not found from the schedule", func() {
+			expectedSchedulerCalls(scheduler)
+			ctx := context.TODO()
+			handler.CleanUpGitRepoPollJobs(ctx)
 		})
-	}
-}
+	})
+})
diff --git a/pkg/git/poll/suite_test.go b/pkg/git/poll/suite_test.go
new file mode 100644
index 00000000..246c07f0
--- /dev/null
+++ b/pkg/git/poll/suite_test.go
@@ -0,0 +1,22 @@
+package poll_test
+
+import (
+	"testing"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+const (
+	timeout = 30 * time.Second
+)
+
+func TestFleet(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Gitrepo pooling Suite")
+}
+
+var _ = BeforeSuite(func() {
+	SetDefaultEventuallyTimeout(timeout)
+})
diff --git a/pkg/git/poll/watch_test.go b/pkg/git/poll/watch_test.go
deleted file mode 100644
index db32ab91..00000000
--- a/pkg/git/poll/watch_test.go
+++ /dev/null
@@ -1,84 +0,0 @@
-//go:generate mockgen --build_flags=--mod=mod -destination=../mocks/fetch_mock.go -package=mocks github.com/rancher/fleet/pkg/git/poll GitFetcher
-
-package poll
-
-import (
-	"context"
-	"sync"
-	"testing"
-	"time"
-
-	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
-	"github.com/rancher/fleet/pkg/git/mocks"
-
-	"go.uber.org/mock/gomock"
-
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/types"
-	"sigs.k8s.io/controller-runtime/pkg/client/fake"
-)
-
-func TestFetchBySyncInterval(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-
-	gitRepo := v1alpha1.GitRepo{
-		ObjectMeta: metav1.ObjectMeta{
-			Name: "gitrepo",
-		},
-	}
-	fetcher := mocks.NewMockGitFetcher(ctrl)
-	scheme := runtime.NewScheme()
-	err := v1alpha1.AddToScheme(scheme)
-	if err != nil {
-		t.Errorf("unexpected error %v", err)
-	}
-	client := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(&gitRepo).WithStatusSubresource(&gitRepo).Build()
-	ctx := context.TODO()
-	commit := "fakeCommit"
-
-	tests := map[string]struct {
-		numTimeTickMock int
-	}{
-		".Status.LastPulledCommit is updated before syncPeriod": {
-			0,
-		},
-		"Latest commit is fetched 10 times": {
-			10,
-		},
-	}
-
-	for name, test := range tests {
-		t.Run(name, func(t *testing.T) {
-			w := Watch{
-				gitRepo: gitRepo,
-				client:  client,
-				mu:      new(sync.Mutex),
-				fetcher: fetcher,
-			}
-			tickerC := make(chan time.Time)
-			ticker := &time.Ticker{
-				C: tickerC,
-			}
-			fetcher.EXPECT().LatestCommit(ctx, gomock.Any(), client).Return(commit, nil).Times(test.numTimeTickMock + 1)
-			go func() {
-				for i := 0; i < test.numTimeTickMock; i++ {
-					tickerC <- time.Now() // simulate pollingInterval has passed
-				}
-				w.Finish()
-			}()
-
-			w.fetchBySyncInterval(ctx, ticker)
-
-			updatedGitRepo := v1alpha1.GitRepo{}
-			err = client.Get(ctx, types.NamespacedName{Name: gitRepo.Name, Namespace: gitRepo.Namespace}, &updatedGitRepo)
-			if err != nil {
-				t.Errorf("unexpected error %v", err)
-			}
-			if updatedGitRepo.Status.Commit != commit {
-				t.Errorf("expected .Status.Commit %v, but got %v", commit, updatedGitRepo.Status.Commit)
-			}
-		})
-	}
-}