Repository: argoproj/argo-cd
Pull request #6266 (2021-05-18 19:10:53)

fix: namespace install shouldn't contain namespace in the manifests
fixes https://github.com/argoproj/argo-cd/issues/6227

Checklist:

* [x] Either (a) I've created an [enhancement proposal](https://github.com/argoproj/argo-cd/issues/new/choose) and discussed it with the community, (b) this is a bug fix, or (c) this does not need to be in the release notes.
* [x] The title of the PR states what changed and the related issues number (used for the release note).
* [x] I've included "Closes [ISSUE #]" or "Fixes [ISSUE #]" in the description to automatically close the associated issue.
* [ ] I've updated both the CLI and UI to expose my feature, or I plan to submit a second PR with them.
* [ ] Does this PR require documentation updates?
* [ ] I've updated documentation as required by this PR.
* [ ] Optional. My organization is added to USERS.md.
* [x] I have signed off all my commits as required by [DCO](https://github.com/argoproj/argoproj/tree/master/community#contributing-to-argo)
* [ ] I have written unit and/or e2e tests for my change. PRs without these are unlikely to be merged.
* [ ] My build is green ([troubleshooting builds](https://argo-cd.readthedocs.io/en/latest/developer-guide/ci/)). 



codecov[bot]: # [Codecov](https://codecov.io/gh/argoproj/argo-cd/pull/6266?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj) Report
> Merging [#6266](https://codecov.io/gh/argoproj/argo-cd/pull/6266?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj) (6504f91) into [master](https://codecov.io/gh/argoproj/argo-cd/commit/d9bc6cf6990639f68b4ba591fe84057176ba1572?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj) (d9bc6cf) will **increase** coverage by `0.12%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/argoproj/argo-cd/pull/6266/graphs/tree.svg?width=650&height=150&src=pr&token=kf5kwuegwE&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj)](https://codecov.io/gh/argoproj/argo-cd/pull/6266?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj)

```diff
@@            Coverage Diff             @@
##           master    #6266      +/-   ##
==========================================
+ Coverage   41.06%   41.18%   +0.12%     
==========================================
  Files         147      148       +1     
  Lines       19935    19874      -61     
==========================================
- Hits         8187     8186       -1     
+ Misses      10621    10568      -53     
+ Partials     1127     1120       -7     
```


| [Impacted Files](https://codecov.io/gh/argoproj/argo-cd/pull/6266?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj) | Coverage Δ | |
|---|---|---|
| [util/git/client.go](https://codecov.io/gh/argoproj/argo-cd/pull/6266/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj#diff-dXRpbC9naXQvY2xpZW50Lmdv) | `47.67% <0.00%> (-0.38%)` | :arrow_down: |
| [reposerver/repository/repository.go](https://codecov.io/gh/argoproj/argo-cd/pull/6266/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj#diff-cmVwb3NlcnZlci9yZXBvc2l0b3J5L3JlcG9zaXRvcnkuZ28=) | `60.60% <0.00%> (-0.17%)` | :arrow_down: |
| [cmd/argocd-util/commands/argocd\_util.go](https://codecov.io/gh/argoproj/argo-cd/pull/6266/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj#diff-Y21kL2FyZ29jZC11dGlsL2NvbW1hbmRzL2FyZ29jZF91dGlsLmdv) | `14.51% <0.00%> (-0.12%)` | :arrow_down: |
| [cmd/argocd/commands/cluster.go](https://codecov.io/gh/argoproj/argo-cd/pull/6266/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj#diff-Y21kL2FyZ29jZC9jb21tYW5kcy9jbHVzdGVyLmdv) | `2.59% <0.00%> (-0.10%)` | :arrow_down: |
| [pkg/apis/application/v1alpha1/types.go](https://codecov.io/gh/argoproj/argo-cd/pull/6266/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj#diff-cGtnL2FwaXMvYXBwbGljYXRpb24vdjFhbHBoYTEvdHlwZXMuZ28=) | `57.38% <0.00%> (-0.07%)` | :arrow_down: |
| [controller/appcontroller.go](https://codecov.io/gh/argoproj/argo-cd/pull/6266/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj#diff-Y29udHJvbGxlci9hcHBjb250cm9sbGVyLmdv) | `53.47% <0.00%> (-0.05%)` | :arrow_down: |
| [cmd/argocd-util/commands/backup.go](https://codecov.io/gh/argoproj/argo-cd/pull/6266/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj#diff-Y21kL2FyZ29jZC11dGlsL2NvbW1hbmRzL2JhY2t1cC5nbw==) | `0.00% <0.00%> (ø)` | |
| [util/git/ssh.go](https://codecov.io/gh/argoproj/argo-cd/pull/6266/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj#diff-dXRpbC9naXQvc3NoLmdv) | `0.00% <0.00%> (ø)` | |
| [util/settings/settings.go](https://codecov.io/gh/argoproj/argo-cd/pull/6266/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj#diff-dXRpbC9zZXR0aW5ncy9zZXR0aW5ncy5nbw==) | `46.75% <0.00%> (+0.09%)` | :arrow_up: |
| [util/db/repository.go](https://codecov.io/gh/argoproj/argo-cd/pull/6266/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj#diff-dXRpbC9kYi9yZXBvc2l0b3J5Lmdv) | `62.73% <0.00%> (+0.23%)` | :arrow_up: |
| ... and [4 more](https://codecov.io/gh/argoproj/argo-cd/pull/6266/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/argoproj/argo-cd/pull/6266?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/argoproj/argo-cd/pull/6266?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj). Last update [d9bc6cf...6504f91](https://codecov.io/gh/argoproj/argo-cd/pull/6266?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj).


Patch:
diff --git a/cmd/argocd-application-controller/commands/argocd_application_controller.go b/cmd/argocd-application-controller/commands/argocd_application_controller.go
index 2e6c5d24b..c9f091603 100644
--- a/cmd/argocd-application-controller/commands/argocd_application_controller.go
+++ b/cmd/argocd-application-controller/commands/argocd_application_controller.go
@@ -137,7 +137,7 @@ func NewCommand() *cobra.Command {
 	}
 
 	clientConfig = cli.AddKubectlFlagsToCmd(&command)
-	command.Flags().Int64Var(&appResyncPeriod, "app-resync", defaultAppResyncPeriod, "Time period in seconds for application resync.")
+	command.Flags().Int64Var(&appResyncPeriod, "app-resync", int64(env.ParseDurationFromEnv("ARGOCD_RECONCILIATION_TIMEOUT", defaultAppResyncPeriod, 0, math.MaxInt32).Seconds()), "Time period in seconds for application resync.")
 	command.Flags().StringVar(&repoServerAddress, "repo-server", common.DefaultRepoServerAddr, "Repo server address.")
 	command.Flags().IntVar(&repoServerTimeoutSeconds, "repo-server-timeout-seconds", 60, "Repo server RPC call timeout seconds.")
 	command.Flags().IntVar(&statusProcessors, "status-processors", 1, "Number of application status processors")
diff --git a/cmd/argocd-util/commands/app.go b/cmd/argocd-util/commands/app.go
index 3a4425e19..2a9ff5fe9 100644
--- a/cmd/argocd-util/commands/app.go
+++ b/cmd/argocd-util/commands/app.go
@@ -21,7 +21,6 @@ import (
 	"k8s.io/client-go/tools/clientcmd"
 
 	cmdutil "github.com/argoproj/argo-cd/v2/cmd/util"
-	"github.com/argoproj/argo-cd/v2/common"
 	"github.com/argoproj/argo-cd/v2/controller"
 	"github.com/argoproj/argo-cd/v2/controller/cache"
 	"github.com/argoproj/argo-cd/v2/controller/metrics"
@@ -237,7 +236,7 @@ func NewReconcileCommand() *cobra.Command {
 			}
 			outputPath := args[0]
 
-			errors.CheckError(os.Setenv(common.EnvVarFakeInClusterConfig, "true"))
+			errors.CheckError(os.Setenv(v1alpha1.EnvVarFakeInClusterConfig, "true"))
 			cfg, err := clientConfig.ClientConfig()
 			errors.CheckError(err)
 			namespace, _, err := clientConfig.Namespace()
@@ -390,7 +389,7 @@ func reconcileApplications(
 			return nil, err
 		}
 
-		res := appStateManager.CompareAppState(&app, proj, app.Spec.Source.TargetRevision, app.Spec.Source, false, nil)
+		res := appStateManager.CompareAppState(&app, proj, app.Spec.Source.TargetRevision, app.Spec.Source, false, false, nil)
 		items = append(items, appReconcileResult{
 			Name:       app.Name,
 			Conditions: app.Status.Conditions,
diff --git a/cmd/argocd-util/commands/cluster.go b/cmd/argocd-util/commands/cluster.go
index 5885a6b50..053f23c4e 100644
--- a/cmd/argocd-util/commands/cluster.go
+++ b/cmd/argocd-util/commands/cluster.go
@@ -17,7 +17,6 @@ import (
 	"k8s.io/client-go/tools/clientcmd"
 
 	cmdutil "github.com/argoproj/argo-cd/v2/cmd/util"
-	"github.com/argoproj/argo-cd/v2/common"
 	"github.com/argoproj/argo-cd/v2/controller/sharding"
 	argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
 	cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
@@ -206,7 +205,7 @@ func NewGenClusterConfigCommand(pathOpts *clientcmd.PathOptions) *cobra.Command
 			}
 			clst := cmdutil.NewCluster(contextName, clusterOpts.Namespaces, conf, bearerToken, awsAuthConf, execProviderConf)
 			if clusterOpts.InCluster {
-				clst.Server = common.KubernetesInternalAPIServerAddr
+				clst.Server = argoappv1.KubernetesInternalAPIServerAddr
 			}
 			if clusterOpts.Shard >= 0 {
 				clst.Shard = &clusterOpts.Shard
diff --git a/cmd/argocd/commands/app.go b/cmd/argocd/commands/app.go
index f9c233277..6b239f037 100644
--- a/cmd/argocd/commands/app.go
+++ b/cmd/argocd/commands/app.go
@@ -31,7 +31,6 @@ import (
 	"k8s.io/utils/pointer"
 
 	cmdutil "github.com/argoproj/argo-cd/v2/cmd/util"
-	"github.com/argoproj/argo-cd/v2/common"
 	"github.com/argoproj/argo-cd/v2/controller"
 	"github.com/argoproj/argo-cd/v2/pkg/apiclient"
 	argocdclient "github.com/argoproj/argo-cd/v2/pkg/apiclient"
@@ -1434,9 +1433,9 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
 	command.Flags().StringArrayVar(&labels, "label", []string{}, "Sync only specific resources with a label. This option may be specified repeatedly.")
 	command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
 	command.Flags().Int64Var(&retryLimit, "retry-limit", 0, "Max number of allowed sync retries")
-	command.Flags().DurationVar(&retryBackoffDuration, "retry-backoff-duration", common.DefaultSyncRetryDuration, "Retry backoff base duration. Input needs to be a duration (e.g. 2m, 1h)")
-	command.Flags().DurationVar(&retryBackoffMaxDuration, "retry-backoff-max-duration", common.DefaultSyncRetryMaxDuration, "Max retry backoff duration. Input needs to be a duration (e.g. 2m, 1h)")
-	command.Flags().Int64Var(&retryBackoffFactor, "retry-backoff-factor", common.DefaultSyncRetryFactor, "Factor multiplies the base duration after each failed retry")
+	command.Flags().DurationVar(&retryBackoffDuration, "retry-backoff-duration", argoappv1.DefaultSyncRetryDuration, "Retry backoff base duration. Input needs to be a duration (e.g. 2m, 1h)")
+	command.Flags().DurationVar(&retryBackoffMaxDuration, "retry-backoff-max-duration", argoappv1.DefaultSyncRetryMaxDuration, "Max retry backoff duration. Input needs to be a duration (e.g. 2m, 1h)")
+	command.Flags().Int64Var(&retryBackoffFactor, "retry-backoff-factor", argoappv1.DefaultSyncRetryFactor, "Factor multiplies the base duration after each failed retry")
 	command.Flags().StringVar(&strategy, "strategy", "", "Sync strategy (one of: apply|hook)")
 	command.Flags().BoolVar(&force, "force", false, "Use a force apply")
 	command.Flags().BoolVar(&async, "async", false, "Do not wait for application to sync before continuing")
diff --git a/cmd/argocd/commands/cluster.go b/cmd/argocd/commands/cluster.go
index 193bbdbf4..6f0563812 100644
--- a/cmd/argocd/commands/cluster.go
+++ b/cmd/argocd/commands/cluster.go
@@ -129,7 +129,7 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
 			}
 			clst := cmdutil.NewCluster(contextName, clusterOpts.Namespaces, conf, managerBearerToken, awsAuthConf, execProviderConf)
 			if clusterOpts.InCluster {
-				clst.Server = common.KubernetesInternalAPIServerAddr
+				clst.Server = argoappv1.KubernetesInternalAPIServerAddr
 			}
 			if clusterOpts.Shard >= 0 {
 				clst.Shard = &clusterOpts.Shard
diff --git a/cmd/util/app.go b/cmd/util/app.go
index 5513845f4..fc5a49289 100644
--- a/cmd/util/app.go
+++ b/cmd/util/app.go
@@ -15,7 +15,6 @@ import (
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/utils/pointer"
 
-	"github.com/argoproj/argo-cd/v2/common"
 	"github.com/argoproj/argo-cd/v2/pkg/apis/application"
 	argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
 	"github.com/argoproj/argo-cd/v2/util/config"
@@ -76,7 +75,7 @@ func AddAppFlags(command *cobra.Command, opts *AppOptions) {
 	command.Flags().StringVar(&opts.chart, "helm-chart", "", "Helm Chart name")
 	command.Flags().StringVar(&opts.env, "env", "", "Application environment to monitor")
 	command.Flags().StringVar(&opts.revision, "revision", "", "The tracking source branch, tag, commit or Helm chart version the application will sync to")
-	command.Flags().IntVar(&opts.revisionHistoryLimit, "revision-history-limit", common.RevisionHistoryLimit, "How many items to keep in revision history")
+	command.Flags().IntVar(&opts.revisionHistoryLimit, "revision-history-limit", argoappv1.RevisionHistoryLimit, "How many items to keep in revision history")
 	command.Flags().StringVar(&opts.destServer, "dest-server", "", "K8s cluster URL (e.g. https://kubernetes.default.svc)")
 	command.Flags().StringVar(&opts.destName, "dest-name", "", "K8s cluster Name (e.g. minikube)")
 	command.Flags().StringVar(&opts.destNamespace, "dest-namespace", "", "K8s target namespace (overrides the namespace specified in the ksonnet app.yaml)")
@@ -112,9 +111,9 @@ func AddAppFlags(command *cobra.Command, opts *AppOptions) {
 	command.Flags().StringVar(&opts.directoryExclude, "directory-exclude", "", "Set glob expression used to exclude files from application source path")
 	command.Flags().StringVar(&opts.directoryInclude, "directory-include", "", "Set glob expression used to include files from application source path")
 	command.Flags().Int64Var(&opts.retryLimit, "sync-retry-limit", 0, "Max number of allowed sync retries")
-	command.Flags().DurationVar(&opts.retryBackoffDuration, "sync-retry-backoff-duration", common.DefaultSyncRetryDuration, "Sync retry backoff base duration. Input needs to be a duration (e.g. 2m, 1h)")
-	command.Flags().DurationVar(&opts.retryBackoffMaxDuration, "sync-retry-backoff-max-duration", common.DefaultSyncRetryMaxDuration, "Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h)")
-	command.Flags().Int64Var(&opts.retryBackoffFactor, "sync-retry-backoff-factor", common.DefaultSyncRetryFactor, "Factor multiplies the base duration after each failed sync retry")
+	command.Flags().DurationVar(&opts.retryBackoffDuration, "sync-retry-backoff-duration", argoappv1.DefaultSyncRetryDuration, "Sync retry backoff base duration. Input needs to be a duration (e.g. 2m, 1h)")
+	command.Flags().DurationVar(&opts.retryBackoffMaxDuration, "sync-retry-backoff-max-duration", argoappv1.DefaultSyncRetryMaxDuration, "Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h)")
+	command.Flags().Int64Var(&opts.retryBackoffFactor, "sync-retry-backoff-factor", argoappv1.DefaultSyncRetryFactor, "Factor multiplies the base duration after each failed sync retry")
 }
 
 func SetAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, appOpts *AppOptions) int {
diff --git a/common/common.go b/common/common.go
index 260a18f6c..61a7b41a9 100644
--- a/common/common.go
+++ b/common/common.go
@@ -2,7 +2,6 @@ package common
 
 import (
 	"os"
-	"strconv"
 	"time"
 )
 
@@ -57,26 +56,16 @@ const (
 	DefaultAppConfigPath = "/app/config"
 )
 
-const (
-	DefaultSyncRetryDuration    = 5 * time.Second
-	DefaultSyncRetryMaxDuration = 3 * time.Minute
-	DefaultSyncRetryFactor      = int64(2)
-)
-
 // Argo CD application related constants
 const (
-	// KubernetesInternalAPIServerAddr is address of the k8s API server when accessing internal to the cluster
-	KubernetesInternalAPIServerAddr = "https://kubernetes.default.svc"
-	// DefaultAppProjectName contains name of 'default' app project, which is available in every Argo CD installation
-	DefaultAppProjectName = "default"
+
 	// ArgoCDAdminUsername is the username of the 'admin' user
 	ArgoCDAdminUsername = "admin"
 	// ArgoCDUserAgentName is the default user-agent name used by the gRPC API client library and grpc-gateway
 	ArgoCDUserAgentName = "argocd-client"
 	// AuthCookieName is the HTTP cookie name where we store our auth token
 	AuthCookieName = "argocd.token"
-	// RevisionHistoryLimit is the max number of successful sync to keep in history
-	RevisionHistoryLimit = 10
+
 	// ChangePasswordSSOTokenMaxAge is the max token age for password change operation
 	ChangePasswordSSOTokenMaxAge = time.Minute * 5
 	// GithubAppCredsExpirationDuration is the default time used to cache the GitHub app credentials
@@ -120,27 +109,10 @@ const (
 	// AnnotationCompareOptions is a comma-separated list of options for comparison
 	AnnotationCompareOptions = "argocd.argoproj.io/compare-options"
 
-	// AnnotationKeyRefresh is the annotation key which indicates that app needs to be refreshed. Removed by application controller after app is refreshed.
-	// Might take values 'normal'/'hard'. Value 'hard' means manifest cache and target cluster state cache should be invalidated before refresh.
-	AnnotationKeyRefresh = "argocd.argoproj.io/refresh"
 	// AnnotationKeyManagedBy is annotation name which indicates that k8s resource is managed by an application.
 	AnnotationKeyManagedBy = "managed-by"
 	// AnnotationValueManagedByArgoCD is a 'managed-by' annotation value for resources managed by Argo CD
 	AnnotationValueManagedByArgoCD = "argocd.argoproj.io"
-	// ResourcesFinalizerName is the finalizer value which we inject to finalize deletion of an application
-	ResourcesFinalizerName = "resources-finalizer.argocd.argoproj.io"
-
-	// ForegroundPropagationPolicyFinalizer is the finalizer we inject to delete application with foreground propagation policy
-	ForegroundPropagationPolicyFinalizer = "resources-finalizer.argocd.argoproj.io/foreground"
-
-	// BackgroundPropagationPolicyFinalizer is the finalizer we inject to delete application with background propagation policy
-	BackgroundPropagationPolicyFinalizer = "resources-finalizer.argocd.argoproj.io/background"
-
-	// AnnotationKeyManifestGeneratePaths is an annotation that contains a list of semicolon-separated paths in the
-	// manifests repository that affects the manifest generation. Paths might be either relative or absolute. The
-	// absolute path means an absolute path within the repository and the relative path is relative to the application
-	// source path within the repository.
-	AnnotationKeyManifestGeneratePaths = "argocd.argoproj.io/manifest-generate-paths"
 
 	// AnnotationKeyLinkPrefix tells the UI to add an external link icon to the application node
 	// that links to the value given in the annotation.
@@ -159,9 +131,6 @@ const (
 	EnvVarSSODebug = "ARGOCD_SSO_DEBUG"
 	// EnvVarRBACDebug is an environment variable to enable additional RBAC debugging in the API server
 	EnvVarRBACDebug = "ARGOCD_RBAC_DEBUG"
-	// EnvVarFakeInClusterConfig is an environment variable to fake an in-cluster RESTConfig using
-	// the current kubectl context (for development purposes)
-	EnvVarFakeInClusterConfig = "ARGOCD_FAKE_IN_CLUSTER"
 	// Overrides the location where SSH known hosts for repo access data is stored
 	EnvVarSSHDataPath = "ARGOCD_SSH_DATA_PATH"
 	// Overrides the location where TLS certificate for repo access data is stored
@@ -170,14 +139,6 @@ const (
 	EnvGitAttemptsCount = "ARGOCD_GIT_ATTEMPTS_COUNT"
 	// Overrides git submodule support, true by default
 	EnvGitSubmoduleEnabled = "ARGOCD_GIT_MODULES_ENABLED"
-	// EnvK8sClientQPS is the QPS value used for the kubernetes client (default: 50)
-	EnvK8sClientQPS = "ARGOCD_K8S_CLIENT_QPS"
-	// EnvK8sClientBurst is the burst value used for the kubernetes client (default: twice the client QPS)
-	EnvK8sClientBurst = "ARGOCD_K8S_CLIENT_BURST"
-	// EnvClusterCacheResyncDuration is the env variable that holds cluster cache re-sync duration
-	EnvClusterCacheResyncDuration = "ARGOCD_CLUSTER_CACHE_RESYNC_DURATION"
-	// EnvK8sClientMaxIdleConnections is the number of max idle connections in K8s REST client HTTP transport (default: 500)
-	EnvK8sClientMaxIdleConnections = "ARGOCD_K8S_CLIENT_MAX_IDLE_CONNECTIONS"
 	// EnvGnuPGHome is the path to ArgoCD's GnuPG keyring for signature verification
 	EnvGnuPGHome = "ARGOCD_GNUPGHOME"
 	// EnvWatchAPIBufferSize is the buffer size used to transfer K8S watch events to watch API consumer
@@ -220,40 +181,3 @@ func GetGnuPGHomePath() string {
 		return gnuPgHome
 	}
 }
-
-var (
-	// K8sClientConfigQPS controls the QPS to be used in K8s REST client configs
-	K8sClientConfigQPS float32 = 50
-	// K8sClientConfigBurst controls the burst to be used in K8s REST client configs
-	K8sClientConfigBurst int = 100
-	// K8sMaxIdleConnections controls the number of max idle connections in K8s REST client HTTP transport
-	K8sMaxIdleConnections = 500
-	// K8sMaxIdleConnections controls the duration of cluster cache refresh
-	K8SClusterResyncDuration = 12 * time.Hour
-)
-
-func init() {
-	if envQPS := os.Getenv(EnvK8sClientQPS); envQPS != "" {
-		if qps, err := strconv.ParseFloat(envQPS, 32); err != nil {
-			K8sClientConfigQPS = float32(qps)
-		}
-	}
-	if envBurst := os.Getenv(EnvK8sClientBurst); envBurst != "" {
-		if burst, err := strconv.Atoi(envBurst); err != nil {
-			K8sClientConfigBurst = burst
-		}
-	} else {
-		K8sClientConfigBurst = 2 * int(K8sClientConfigQPS)
-	}
-
-	if envMaxConn := os.Getenv(EnvK8sClientMaxIdleConnections); envMaxConn != "" {
-		if maxConn, err := strconv.Atoi(envMaxConn); err != nil {
-			K8sMaxIdleConnections = maxConn
-		}
-	}
-	if clusterResyncDurationStr := os.Getenv(EnvClusterCacheResyncDuration); clusterResyncDurationStr != "" {
-		if duration, err := time.ParseDuration(clusterResyncDurationStr); err == nil {
-			K8SClusterResyncDuration = duration
-		}
-	}
-}
diff --git a/controller/appcontroller.go b/controller/appcontroller.go
index d6827ccbb..41574556e 100644
--- a/controller/appcontroller.go
+++ b/controller/appcontroller.go
@@ -39,7 +39,6 @@ import (
 	// make sure to register workqueue prometheus metrics
 	_ "k8s.io/component-base/metrics/prometheus/workqueue"
 
-	"github.com/argoproj/argo-cd/v2/common"
 	statecache "github.com/argoproj/argo-cd/v2/controller/cache"
 	"github.com/argoproj/argo-cd/v2/controller/metrics"
 	"github.com/argoproj/argo-cd/v2/pkg/apis/application"
@@ -66,6 +65,8 @@ const (
 type CompareWith int
 
 const (
+	// Compare live application state against state defined in latest git revision with no resolved revision caching.
+	CompareWithLatestForceResolve CompareWith = 3
 	// Compare live application state against state defined in latest git revision.
 	CompareWithLatest CompareWith = 2
 	// Compare live application state against state defined using revision of most recent comparison.
@@ -842,7 +843,7 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
 	filteredObjs := FilterObjectsForDeletion(objs)
 
 	propagationPolicy := metav1.DeletePropagationForeground
-	if app.GetPropagationPolicy() == common.BackgroundPropagationPolicyFinalizer {
+	if app.GetPropagationPolicy() == appv1.BackgroundPropagationPolicyFinalizer {
 		propagationPolicy = metav1.DeletePropagationBackground
 	}
 	logCtx.Infof("Deleting application's resources with %s propagation policy", propagationPolicy)
@@ -1199,7 +1200,9 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
 	}
 
 	now := metav1.Now()
-	compareResult := ctrl.appStateManager.CompareAppState(app, project, revision, app.Spec.Source, refreshType == appv1.RefreshTypeHard, localManifests)
+	compareResult := ctrl.appStateManager.CompareAppState(app, project, revision, app.Spec.Source,
+		refreshType == appv1.RefreshTypeHard,
+		comparisonLevel == CompareWithLatestForceResolve, localManifests)
 	for k, v := range compareResult.timings {
 		logCtx = logCtx.WithField(k, v.Milliseconds())
 	}
@@ -1230,7 +1233,7 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
 		logCtx.Info("Sync prevented by sync window")
 	}
 
-	if app.Status.ReconciledAt == nil || comparisonLevel == CompareWithLatest {
+	if app.Status.ReconciledAt == nil || comparisonLevel >= CompareWithLatest {
 		app.Status.ReconciledAt = &now
 	}
 	app.Status.Sync = *compareResult.syncStatus
@@ -1260,9 +1263,13 @@ func (ctrl *ApplicationController) needRefreshAppStatus(app *appv1.Application,
 	expired := app.Status.ReconciledAt == nil || app.Status.ReconciledAt.Add(statusRefreshTimeout).Before(time.Now().UTC())
 
 	if requestedType, ok := app.IsRefreshRequested(); ok {
+		compareWith = CompareWithLatestForceResolve
 		// user requested app refresh.
 		refreshType = requestedType
 		reason = fmt.Sprintf("%s refresh requested", refreshType)
+	} else if !app.Spec.Source.Equals(app.Status.Sync.ComparedTo.Source) {
+		reason = "spec.source differs"
+		compareWith = CompareWithLatestForceResolve
 	} else if expired {
 		// The commented line below mysteriously crashes if app.Status.ReconciledAt is nil
 		// reason = fmt.Sprintf("comparison expired. reconciledAt: %v, expiry: %v", app.Status.ReconciledAt, statusRefreshTimeout)
@@ -1272,8 +1279,6 @@ func (ctrl *ApplicationController) needRefreshAppStatus(app *appv1.Application,
 			reconciledAtStr = app.Status.ReconciledAt.String()
 		}
 		reason = fmt.Sprintf("comparison expired. reconciledAt: %v, expiry: %v", reconciledAtStr, statusRefreshTimeout)
-	} else if !app.Spec.Source.Equals(app.Status.Sync.ComparedTo.Source) {
-		reason = "spec.source differs"
 	} else if !app.Spec.Destination.Equals(app.Status.Sync.ComparedTo.Destination) {
 		reason = "spec.destination differs"
 	} else if requested, level := ctrl.isRefreshRequested(app.Name); requested {
@@ -1356,7 +1361,7 @@ func (ctrl *ApplicationController) persistAppStatus(orig *appv1.Application, new
 		for k, v := range orig.GetAnnotations() {
 			newAnnotations[k] = v
 		}
-		delete(newAnnotations, common.AnnotationKeyRefresh)
+		delete(newAnnotations, appv1.AnnotationKeyRefresh)
 	}
 	patch, modified, err := diff.CreateTwoWayMergePatch(
 		&appv1.Application{ObjectMeta: metav1.ObjectMeta{Annotations: orig.GetAnnotations()}, Status: orig.Status},
diff --git a/controller/cache/cache.go b/controller/cache/cache.go
index f59cf08df..6af23f811 100644
--- a/controller/cache/cache.go
+++ b/controller/cache/cache.go
@@ -3,8 +3,10 @@ package cache
 import (
 	"context"
 	"fmt"
+	"os"
 	"reflect"
 	"sync"
+	"time"
 
 	clustercache "github.com/argoproj/gitops-engine/pkg/cache"
 	"github.com/argoproj/gitops-engine/pkg/health"
@@ -18,7 +20,6 @@ import (
 	"k8s.io/apimachinery/pkg/watch"
 	"k8s.io/client-go/tools/cache"
 
-	"github.com/argoproj/argo-cd/v2/common"
 	"github.com/argoproj/argo-cd/v2/controller/metrics"
 	appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
 	"github.com/argoproj/argo-cd/v2/util/argo"
@@ -28,6 +29,25 @@ import (
 	"github.com/argoproj/argo-cd/v2/util/settings"
 )
 
+const (
+	// EnvClusterCacheResyncDuration is the env variable that holds cluster cache re-sync duration
+	EnvClusterCacheResyncDuration = "ARGOCD_CLUSTER_CACHE_RESYNC_DURATION"
+)
+
+var (
+	// K8SClusterResyncDuration controls the duration of cluster cache refresh
+	K8SClusterResyncDuration = 12 * time.Hour
+)
+
+func init() {
+
+	if clusterResyncDurationStr := os.Getenv(EnvClusterCacheResyncDuration); clusterResyncDurationStr != "" {
+		if duration, err := time.ParseDuration(clusterResyncDurationStr); err == nil {
+			K8SClusterResyncDuration = duration
+		}
+	}
+}
+
 type LiveStateCache interface {
 	// Returns k8s server version
 	GetVersionsInfo(serverURL string) (string, []metav1.APIGroup, error)
@@ -267,7 +287,7 @@ func (c *liveStateCache) getCluster(server string) (clustercache.ClusterCache, e
 
 	clusterCache = clustercache.NewClusterCache(cluster.RESTConfig(),
 		clustercache.SetListSemaphore(c.listSemaphore),
-		clustercache.SetResyncTimeout(common.K8SClusterResyncDuration),
+		clustercache.SetResyncTimeout(K8SClusterResyncDuration),
 		clustercache.SetSettings(cacheSettings.clusterSettings),
 		clustercache.SetNamespaces(cluster.Namespaces),
 		clustercache.SetPopulateResourceInfoHandler(func(un *unstructured.Unstructured, isRoot bool) (interface{}, bool) {
diff --git a/controller/state.go b/controller/state.go
index 1a6f88b3e..19a6a2a1e 100644
--- a/controller/state.go
+++ b/controller/state.go
@@ -75,7 +75,7 @@ func GetLiveObjsForApplicationHealth(resources []managedResource, statuses []app
 
 // AppStateManager defines methods which allow to compare application spec and actual application state.
 type AppStateManager interface {
-	CompareAppState(app *v1alpha1.Application, project *appv1.AppProject, revision string, source v1alpha1.ApplicationSource, noCache bool, localObjects []string) *comparisonResult
+	CompareAppState(app *v1alpha1.Application, project *appv1.AppProject, revision string, source v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localObjects []string) *comparisonResult
 	SyncAppState(app *v1alpha1.Application, state *v1alpha1.OperationState)
 }
 
@@ -115,7 +115,7 @@ type appStateManager struct {
 	statusRefreshTimeout time.Duration
 }
 
-func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1.ApplicationSource, appLabelKey, revision string, noCache, verifySignature bool, proj *v1alpha1.AppProject) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error) {
+func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1.ApplicationSource, appLabelKey, revision string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error) {
 	ts := stats.NewTimingStats()
 	helmRepos, err := m.db.ListHelmRepositories(context.Background())
 	if err != nil {
@@ -178,6 +178,7 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
 		Repos:             permittedHelmRepos,
 		Revision:          revision,
 		NoCache:           noCache,
+		NoRevisionCache:   noRevisionCache,
 		AppLabelKey:       appLabelKey,
 		AppName:           app.Name,
 		Namespace:         app.Spec.Destination.Namespace,
@@ -375,7 +376,7 @@ func (m *appStateManager) diffArrayCached(configArray []*unstructured.Unstructur
 // CompareAppState compares application git state to the live app state, using the specified
 // revision and supplied source. If revision or overrides are empty, then compares against
 // revision and overrides in the app spec.
-func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *appv1.AppProject, revision string, source v1alpha1.ApplicationSource, noCache bool, localManifests []string) *comparisonResult {
+func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *appv1.AppProject, revision string, source v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localManifests []string) *comparisonResult {
 	ts := stats.NewTimingStats()
 	appLabelKey, resourceOverrides, diffNormalizer, resFilter, err := m.getComparisonSettings(app)
 	ts.AddCheckpoint("settings_ms")
@@ -409,7 +410,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
 	now := metav1.Now()
 
 	if len(localManifests) == 0 {
-		targetObjs, manifestInfo, err = m.getRepoObjs(app, source, appLabelKey, revision, noCache, verifySignature, project)
+		targetObjs, manifestInfo, err = m.getRepoObjs(app, source, appLabelKey, revision, noCache, noRevisionCache, verifySignature, project)
 		if err != nil {
 			targetObjs = make([]*unstructured.Unstructured, 0)
 			conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
diff --git a/controller/sync.go b/controller/sync.go
index ea1f49e0f..2e2919db3 100644
--- a/controller/sync.go
+++ b/controller/sync.go
@@ -85,7 +85,7 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
 		return
 	}
 
-	compareResult := m.CompareAppState(app, proj, revision, source, false, syncOp.Manifests)
+	compareResult := m.CompareAppState(app, proj, revision, source, false, true, syncOp.Manifests)
 	// We now have a concrete commit SHA. Save this in the sync result revision so that we remember
 	// what we should be syncing to when resuming operations.
 	syncRes.Revision = compareResult.syncStatus.Revision
diff --git a/docs/operator-manual/argocd-cm.yaml b/docs/operator-manual/argocd-cm.yaml
index 45eb0da46..f0e01fe2a 100644
--- a/docs/operator-manual/argocd-cm.yaml
+++ b/docs/operator-manual/argocd-cm.yaml
@@ -259,3 +259,8 @@ data:
   # Optional link for banner. If set, the entire banner text will become a link.
   # You can have bannercontent without a bannerurl, but not the other way around.
   ui.bannerurl: "https://argoproj.github.io"
+
+  # Application reconciliation timeout is the max amount of time required to discover if a new manifests version got
+  # published to the repository. Three minutes by default.
+  # > Note: argocd-repo-server deployment must be manually restarted after changing the setting.
+  timeout.reconciliation: 180s
diff --git a/docs/operator-manual/high_availability.md b/docs/operator-manual/high_availability.md
index c0e8e58ef..f3c6c4f72 100644
--- a/docs/operator-manual/high_availability.md
+++ b/docs/operator-manual/high_availability.md
@@ -63,7 +63,7 @@ performance. For performance reasons controller monitors and caches only preferr
 preferred version into a version of the resource stored in Git. If `kubectl convert` fails because conversion is not supported than controller fallback to Kubernetes API query which slows down
 reconciliation. In this case advice user-preferred resource version in Git.
 
-* The controller polls Git every 3m by default. You can increase this duration using `--app-resync seconds` to reduce polling.
+* The controller polls Git every 3m by default. You can increase this duration using `timeout.reconciliation` setting in the `argocd-cm` ConfigMap.
 
 * If the controller is managing too many clusters and uses too much memory then you can shard clusters across multiple
 controller replicas. To enable sharding increase the number of replicas in `argocd-application-controller` `StatefulSet`
@@ -135,13 +135,9 @@ with the changed files specified in the webhook payload. If non of the changed f
 
 Installations that use a different repo for each app are **not** subject to this behavior and will likely get no benefit from using these annotations.
 
-!!! note
-    Installations with a large number of apps should also set the `--app-resync` flag in the `argocd-application-controller` process to a larger value to reduce automatic refreshes based on git polling. The exact value is a trade-off between reduced work and app sync in case of a missed webhook event. For most cases `1800` (30m) or `3600` (1h) is a good trade-off.
-
-
 !!! note
     Application manifest paths annotation support depends on the git provider used for the Application. It is currently only supported for GitHub, GitLab, and Gogs based repos
-
+I'm using `.Second()` modifier to avoid distracting users who already rely on `--app-resync` flag.
 * **Relative path** The annotation might contains relative path. In this case the path is considered relative to the path specified in the application source:
 
 ```yaml
diff --git a/docs/operator-manual/server-commands/argocd-application-controller.md b/docs/operator-manual/server-commands/argocd-application-controller.md
index c9bb6b2d0..8d9f9139f 100644
--- a/docs/operator-manual/server-commands/argocd-application-controller.md
+++ b/docs/operator-manual/server-commands/argocd-application-controller.md
@@ -13,7 +13,7 @@ argocd-application-controller [flags]
 ### Options
 
 ```
-      --app-resync int                        Time period in seconds for application resync. (default 180)
+      --app-resync int                        Time period in seconds for application resync.
       --app-state-cache-expiration duration   Cache expiration for app state (default 1h0m0s)
       --as string                             Username to impersonate for the operation
       --as-group stringArray                  Group to impersonate for the operation, this flag can be repeated to specify multiple groups.
diff --git a/docs/operator-manual/server-commands/argocd-repo-server.md b/docs/operator-manual/server-commands/argocd-repo-server.md
index 4294eb4c8..15b381d55 100644
--- a/docs/operator-manual/server-commands/argocd-repo-server.md
+++ b/docs/operator-manual/server-commands/argocd-repo-server.md
@@ -13,21 +13,22 @@ argocd-repo-server [flags]
 ### Options
 
 ```
-      --default-cache-expiration duration   Cache expiration default (default 24h0m0s)
-      --disable-tls                         Disable TLS on the gRPC endpoint
-  -h, --help                                help for argocd-repo-server
-      --logformat string                    Set the logging format. One of: text|json (default "text")
-      --loglevel string                     Set the logging level. One of: debug|info|warn|error (default "info")
-      --metrics-port int                    Start metrics server on given port (default 8084)
-      --parallelismlimit int                Limit on number of concurrent manifests generate requests. Any value less the 1 means no limit.
-      --port int                            Listen on given port for incoming connections (default 8081)
-      --redis string                        Redis server hostname and port (e.g. argocd-redis:6379). 
-      --redisdb int                         Redis database.
-      --repo-cache-expiration duration      Cache expiration for repo state, incl. app lists, app details, manifest generation, revision meta-data (default 24h0m0s)
-      --sentinel stringArray                Redis sentinel hostname and port (e.g. argocd-redis-ha-announce-0:6379). 
-      --sentinelmaster string               Redis sentinel master group name. (default "master")
-      --tlsciphers string                   The list of acceptable ciphers to be used when establishing TLS connections. Use 'list' to list available ciphers. (default "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_RSA_WITH_AES_256_GCM_SHA384")
-      --tlsmaxversion string                The maximum SSL/TLS version that is acceptable (one of: 1.0|1.1|1.2|1.3) (default "1.3")
-      --tlsminversion string                The minimum SSL/TLS version that is acceptable (one of: 1.0|1.1|1.2|1.3) (default "1.2")
+      --default-cache-expiration duration    Cache expiration default (default 24h0m0s)
+      --disable-tls                          Disable TLS on the gRPC endpoint
+  -h, --help                                 help for argocd-repo-server
+      --logformat string                     Set the logging format. One of: text|json (default "text")
+      --loglevel string                      Set the logging level. One of: debug|info|warn|error (default "info")
+      --metrics-port int                     Start metrics server on given port (default 8084)
+      --parallelismlimit int                 Limit on number of concurrent manifests generate requests. Any value less the 1 means no limit.
+      --port int                             Listen on given port for incoming connections (default 8081)
+      --redis string                         Redis server hostname and port (e.g. argocd-redis:6379). 
+      --redisdb int                          Redis database.
+      --repo-cache-expiration duration       Cache expiration for repo state, incl. app lists, app details, manifest generation, revision meta-data (default 24h0m0s)
+      --revision-cache-expiration duration   Cache expiration for cached revision (default 3m0s)
+      --sentinel stringArray                 Redis sentinel hostname and port (e.g. argocd-redis-ha-announce-0:6379). 
+      --sentinelmaster string                Redis sentinel master group name. (default "master")
+      --tlsciphers string                    The list of acceptable ciphers to be used when establishing TLS connections. Use 'list' to list available ciphers. (default "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_RSA_WITH_AES_256_GCM_SHA384")
+      --tlsmaxversion string                 The maximum SSL/TLS version that is acceptable (one of: 1.0|1.1|1.2|1.3) (default "1.3")
+      --tlsminversion string                 The minimum SSL/TLS version that is acceptable (one of: 1.0|1.1|1.2|1.3) (default "1.2")
 ```
 
diff --git a/docs/operator-manual/upgrading/2.0-2.1.md b/docs/operator-manual/upgrading/2.0-2.1.md
index a2878666c..069a06bdc 100644
--- a/docs/operator-manual/upgrading/2.0-2.1.md
+++ b/docs/operator-manual/upgrading/2.0-2.1.md
@@ -5,4 +5,11 @@
 Note that bundled Kustomize has been upgraded to v4.1.2. Some of the flags are changed in Kustomize V4. 
 For example flag name `load_restrictor` is changed in Kustomize v4+. It is changed from `--load_restrictor=none` to `--load-restrictor LoadRestrictionsNone`. 
 
+## Replacing `--app-resync` flag with `timeout.reconciliation` setting
+
+The`--app-resync` flag allows controlling how frequently Argo CD application controller checks resolve the target
+application revision of each application. In order to allow caching resolved revision per repository as opposed to per
+application, the `--app-resync` flag has been deprecated. Please use `timeout.reconciliation` setting in `argocd-cm` ConfigMap instead.
+See example in [argocd-cm.yaml](../argocd-cm.yaml).
+
 From here on you can follow the [regular upgrade process](./overview.md).
\ No newline at end of file
diff --git a/manifests/base/application-controller/argocd-application-controller-statefulset.yaml b/manifests/base/application-controller/argocd-application-controller-statefulset.yaml
index e68c1e573..94877b0c9 100644
--- a/manifests/base/application-controller/argocd-application-controller-statefulset.yaml
+++ b/manifests/base/application-controller/argocd-application-controller-statefulset.yaml
@@ -24,6 +24,13 @@ spec:
         - "20"
         - --operation-processors
         - "10"
+        env:
+        - name: ARGOCD_RECONCILIATION_TIMEOUT
+          valueFrom:
+            configMapKeyRef:
+              name: argocd-cm
+              key: timeout.reconciliation
+              optional: true
         image: quay.io/argoproj/argocd:latest
         imagePullPolicy: Always
         name: argocd-application-controller
diff --git a/manifests/base/repo-server/argocd-repo-server-deployment.yaml b/manifests/base/repo-server/argocd-repo-server-deployment.yaml
index 6113c4543..6c52aa072 100644
--- a/manifests/base/repo-server/argocd-repo-server-deployment.yaml
+++ b/manifests/base/repo-server/argocd-repo-server-deployment.yaml
@@ -25,6 +25,13 @@ spec:
         - argocd-repo-server
         - --redis
         - $(ARGOCD_REDIS_SERVICE):6379
+        env:
+          - name: ARGOCD_RECONCILIATION_TIMEOUT
+            valueFrom:
+              configMapKeyRef:
+                name: argocd-cm
+                key: timeout.reconciliation
+                optional: true
         ports:
         - containerPort: 8081
         - containerPort: 8084
@@ -42,6 +49,8 @@ spec:
           initialDelaySeconds: 5
           periodSeconds: 10
         securityContext:
+          runAsNonRoot: true
+          readOnlyRootFilesystem: true
           allowPrivilegeEscalation: false
           capabilities:
             drop:
@@ -57,6 +66,8 @@ spec:
           mountPath: /app/config/gpg/keys
         - name: argocd-repo-server-tls
           mountPath: /app/config/reposerver/tls
+        - name: tmp
+          mountPath: /tmp
       volumes:
         - name: ssh-known-hosts
           configMap:
@@ -69,6 +80,8 @@ spec:
             name: argocd-gpg-keys-cm
         - name: gpg-keyring
           emptyDir: {}
+        - name: tmp
+          emptyDir: {}
         - name: argocd-repo-server-tls
           secret:
             secretName: argocd-repo-server-tls
diff --git a/manifests/ha/base/redis-ha/kustomization.yaml b/manifests/ha/base/redis-ha/kustomization.yaml
index e8da2d3cc..7d7a4a3ce 100644
--- a/manifests/ha/base/redis-ha/kustomization.yaml
+++ b/manifests/ha/base/redis-ha/kustomization.yaml
@@ -7,6 +7,13 @@ resources:
 - argocd-redis-ha-server-network-policy.yaml
 
 patchesJson6902:
+- target:
+    version: v1
+    group: ""
+    kind: ConfigMap
+    name: argocd-redis-ha-health-configmap
+    namespace: argocd
+  path: overlays/remove-namespace.yaml
 - target:
     version: v1
     group: ""
diff --git a/manifests/ha/install.yaml b/manifests/ha/install.yaml
index c869a29c3..824254012 100644
--- a/manifests/ha/install.yaml
+++ b/manifests/ha/install.yaml
@@ -2815,54 +2815,6 @@ subjects:
   namespace: argocd
 ---
 apiVersion: v1
-data:
-  redis_liveness.sh: |
-    response=$(
-      redis-cli \
-        -h localhost \
-        -p 6379 \
-        ping
-    )
-    if [ "$response" != "PONG" ] && [ "${response:0:7}" != "LOADING" ] ; then
-      echo "$response"
-      exit 1
-    fi
-    echo "response=$response"
-  redis_readiness.sh: |
-    response=$(
-      redis-cli \
-        -h localhost \
-        -p 6379 \
-        ping
-    )
-    if [ "$response" != "PONG" ] ; then
-      echo "$response"
-      exit 1
-    fi
-    echo "response=$response"
-  sentinel_liveness.sh: |
-    response=$(
-      redis-cli \
-        -h localhost \
-        -p 26379 \
-        ping
-    )
-    if [ "$response" != "PONG" ]; then
-      echo "$response"
-      exit 1
-    fi
-    echo "response=$response"
-kind: ConfigMap
-metadata:
-  labels:
-    app: argocd-redis-ha
-    chart: redis-ha-4.12.15
-    heritage: Helm
-    release: argocd
-  name: argocd-redis-ha-health-configmap
-  namespace: argocd
----
-apiVersion: v1
 kind: ConfigMap
 metadata:
   labels:
@@ -3309,6 +3261,53 @@ metadata:
   name: argocd-redis-ha-configmap
 ---
 apiVersion: v1
+data:
+  redis_liveness.sh: |
+    response=$(
+      redis-cli \
+        -h localhost \
+        -p 6379 \
+        ping
+    )
+    if [ "$response" != "PONG" ] && [ "${response:0:7}" != "LOADING" ] ; then
+      echo "$response"
+      exit 1
+    fi
+    echo "response=$response"
+  redis_readiness.sh: |
+    response=$(
+      redis-cli \
+        -h localhost \
+        -p 6379 \
+        ping
+    )
+    if [ "$response" != "PONG" ] ; then
+      echo "$response"
+      exit 1
+    fi
+    echo "response=$response"
+  sentinel_liveness.sh: |
+    response=$(
+      redis-cli \
+        -h localhost \
+        -p 26379 \
+        ping
+    )
+    if [ "$response" != "PONG" ]; then
+      echo "$response"
+      exit 1
+    fi
+    echo "response=$response"
+kind: ConfigMap
+metadata:
+  labels:
+    app: argocd-redis-ha
+    chart: redis-ha-4.12.15
+    heritage: Helm
+    release: argocd
+  name: argocd-redis-ha-health-configmap
+---
+apiVersion: v1
 data:
   ssh_known_hosts: |
     bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAubiN81eDcafrgMeLzaFPsw2kNvEcqTKl/VqLat/MaB33pZy0y3rJZtnqwR2qOOvbwKZYKiEO1O6VqNEBxKvJJelCq0dTXWT5pbO2gDXC6h6QDXCaHo6pOHGPUy+YBaGQRGuSusMEASYiWunYN0vCAI8QaXnWMXNMdFP3jHAJH0eDsoiGnLPBlBp4TNm6rYI74nMzgz3B9IikW4WVK+dc8KZJZWYjAuORU3jc1c/NPskD2ASinf8v3xnfXeukU0sJ5N6m5E8VLjObPEO+mN2t/FZTMZLiFqPWc/ALSqnMnnhwrNi2rbfg/rd/IpL8Le3pSBne8+seeFVBoGqzHM9yXw==
@@ -3755,6 +3754,13 @@ spec:
         - argocd-repo-server
         - --redis
         - argocd-redis-ha-haproxy:6379
+        env:
+        - name: ARGOCD_RECONCILIATION_TIMEOUT
+          valueFrom:
+            configMapKeyRef:
+              key: timeout.reconciliation
+              name: argocd-cm
+              optional: true
         image: quay.io/argoproj/argocd:latest
         imagePullPolicy: Always
         livenessProbe:
@@ -3779,6 +3785,8 @@ spec:
           capabilities:
             drop:
             - all
+          readOnlyRootFilesystem: true
+          runAsNonRoot: true
         volumeMounts:
         - mountPath: /app/config/ssh
           name: ssh-known-hosts
@@ -3790,6 +3798,8 @@ spec:
           name: gpg-keyring
         - mountPath: /app/config/reposerver/tls
           name: argocd-repo-server-tls
+        - mountPath: /tmp
+          name: tmp
       volumes:
       - configMap:
           name: argocd-ssh-known-hosts-cm
@@ -3802,6 +3812,8 @@ spec:
         name: gpg-keys
       - emptyDir: {}
         name: gpg-keyring
+      - emptyDir: {}
+        name: tmp
       - name: argocd-repo-server-tls
         secret:
           items:
@@ -3961,6 +3973,13 @@ spec:
         - "10"
         - --redis
         - argocd-redis-ha-haproxy:6379
+        env:
+        - name: ARGOCD_RECONCILIATION_TIMEOUT
+          valueFrom:
+            configMapKeyRef:
+              key: timeout.reconciliation
+              name: argocd-cm
+              optional: true
         image: quay.io/argoproj/argocd:latest
         imagePullPolicy: Always
         livenessProbe:
diff --git a/manifests/ha/namespace-install.yaml b/manifests/ha/namespace-install.yaml
index eeea83dec..67f9b0c04 100644
--- a/manifests/ha/namespace-install.yaml
+++ b/manifests/ha/namespace-install.yaml
@@ -262,54 +262,6 @@ subjects:
   name: argocd-server
 ---
 apiVersion: v1
-data:
-  redis_liveness.sh: |
-    response=$(
-      redis-cli \
-        -h localhost \
-        -p 6379 \
-        ping
-    )
-    if [ "$response" != "PONG" ] && [ "${response:0:7}" != "LOADING" ] ; then
-      echo "$response"
-      exit 1
-    fi
-    echo "response=$response"
-  redis_readiness.sh: |
-    response=$(
-      redis-cli \
-        -h localhost \
-        -p 6379 \
-        ping
-    )
-    if [ "$response" != "PONG" ] ; then
-      echo "$response"
-      exit 1
-    fi
-    echo "response=$response"
-  sentinel_liveness.sh: |
-    response=$(
-      redis-cli \
-        -h localhost \
-        -p 26379 \
-        ping
-    )
-    if [ "$response" != "PONG" ]; then
-      echo "$response"
-      exit 1
-    fi
-    echo "response=$response"
-kind: ConfigMap
-metadata:
-  labels:
-    app: argocd-redis-ha
-    chart: redis-ha-4.12.15
-    heritage: Helm
-    release: argocd
-  name: argocd-redis-ha-health-configmap
-  namespace: argocd
----
-apiVersion: v1
 kind: ConfigMap
 metadata:
   labels:
@@ -756,6 +708,53 @@ metadata:
   name: argocd-redis-ha-configmap
 ---
 apiVersion: v1
+data:
+  redis_liveness.sh: |
+    response=$(
+      redis-cli \
+        -h localhost \
+        -p 6379 \
+        ping
+    )
+    if [ "$response" != "PONG" ] && [ "${response:0:7}" != "LOADING" ] ; then
+      echo "$response"
+      exit 1
+    fi
+    echo "response=$response"
+  redis_readiness.sh: |
+    response=$(
+      redis-cli \
+        -h localhost \
+        -p 6379 \
+        ping
+    )
+    if [ "$response" != "PONG" ] ; then
+      echo "$response"
+      exit 1
+    fi
+    echo "response=$response"
+  sentinel_liveness.sh: |
+    response=$(
+      redis-cli \
+        -h localhost \
+        -p 26379 \
+        ping
+    )
+    if [ "$response" != "PONG" ]; then
+      echo "$response"
+      exit 1
+    fi
+    echo "response=$response"
+kind: ConfigMap
+metadata:
+  labels:
+    app: argocd-redis-ha
+    chart: redis-ha-4.12.15
+    heritage: Helm
+    release: argocd
+  name: argocd-redis-ha-health-configmap
+---
+apiVersion: v1
 data:
   ssh_known_hosts: |
     bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAubiN81eDcafrgMeLzaFPsw2kNvEcqTKl/VqLat/MaB33pZy0y3rJZtnqwR2qOOvbwKZYKiEO1O6VqNEBxKvJJelCq0dTXWT5pbO2gDXC6h6QDXCaHo6pOHGPUy+YBaGQRGuSusMEASYiWunYN0vCAI8QaXnWMXNMdFP3jHAJH0eDsoiGnLPBlBp4TNm6rYI74nMzgz3B9IikW4WVK+dc8KZJZWYjAuORU3jc1c/NPskD2ASinf8v3xnfXeukU0sJ5N6m5E8VLjObPEO+mN2t/FZTMZLiFqPWc/ALSqnMnnhwrNi2rbfg/rd/IpL8Le3pSBne8+seeFVBoGqzHM9yXw==
@@ -1202,6 +1201,13 @@ spec:
         - argocd-repo-server
         - --redis
         - argocd-redis-ha-haproxy:6379
+        env:
+        - name: ARGOCD_RECONCILIATION_TIMEOUT
+          valueFrom:
+            configMapKeyRef:
+              key: timeout.reconciliation
+              name: argocd-cm
+              optional: true
         image: quay.io/argoproj/argocd:latest
         imagePullPolicy: Always
         livenessProbe:
@@ -1226,6 +1232,8 @@ spec:
           capabilities:
             drop:
             - all
+          readOnlyRootFilesystem: true
+          runAsNonRoot: true
         volumeMounts:
         - mountPath: /app/config/ssh
           name: ssh-known-hosts
@@ -1237,6 +1245,8 @@ spec:
           name: gpg-keyring
         - mountPath: /app/config/reposerver/tls
           name: argocd-repo-server-tls
+        - mountPath: /tmp
+          name: tmp
       volumes:
       - configMap:
           name: argocd-ssh-known-hosts-cm
@@ -1249,6 +1259,8 @@ spec:
         name: gpg-keys
       - emptyDir: {}
         name: gpg-keyring
+      - emptyDir: {}
+        name: tmp
       - name: argocd-repo-server-tls
         secret:
           items:
@@ -1408,6 +1420,13 @@ spec:
         - "10"
         - --redis
         - argocd-redis-ha-haproxy:6379
+        env:
+        - name: ARGOCD_RECONCILIATION_TIMEOUT
+          valueFrom:
+            configMapKeyRef:
+              key: timeout.reconciliation
+              name: argocd-cm
+              optional: true
         image: quay.io/argoproj/argocd:latest
         imagePullPolicy: Always
         livenessProbe:
diff --git a/manifests/install.yaml b/manifests/install.yaml
index f16f3f1fe..51489ff12 100644
--- a/manifests/install.yaml
+++ b/manifests/install.yaml
@@ -3083,6 +3083,13 @@ spec:
         - argocd-repo-server
         - --redis
         - argocd-redis:6379
+        env:
+        - name: ARGOCD_RECONCILIATION_TIMEOUT
+          valueFrom:
+            configMapKeyRef:
+              key: timeout.reconciliation
+              name: argocd-cm
+              optional: true
         image: quay.io/argoproj/argocd:latest
         imagePullPolicy: Always
         livenessProbe:
@@ -3107,6 +3114,8 @@ spec:
           capabilities:
             drop:
             - all
+          readOnlyRootFilesystem: true
+          runAsNonRoot: true
         volumeMounts:
         - mountPath: /app/config/ssh
           name: ssh-known-hosts
@@ -3118,6 +3127,8 @@ spec:
           name: gpg-keyring
         - mountPath: /app/config/reposerver/tls
           name: argocd-repo-server-tls
+        - mountPath: /tmp
+          name: tmp
       volumes:
       - configMap:
           name: argocd-ssh-known-hosts-cm
@@ -3130,6 +3141,8 @@ spec:
         name: gpg-keys
       - emptyDir: {}
         name: gpg-keyring
+      - emptyDir: {}
+        name: tmp
       - name: argocd-repo-server-tls
         secret:
           items:
@@ -3282,6 +3295,13 @@ spec:
         - "20"
         - --operation-processors
         - "10"
+        env:
+        - name: ARGOCD_RECONCILIATION_TIMEOUT
+          valueFrom:
+            configMapKeyRef:
+              key: timeout.reconciliation
+              name: argocd-cm
+              optional: true
         image: quay.io/argoproj/argocd:latest
         imagePullPolicy: Always
         livenessProbe:
diff --git a/manifests/namespace-install.yaml b/manifests/namespace-install.yaml
index 7a201de5f..8ae6c0393 100644
--- a/manifests/namespace-install.yaml
+++ b/manifests/namespace-install.yaml
@@ -530,6 +530,13 @@ spec:
         - argocd-repo-server
         - --redis
         - argocd-redis:6379
+        env:
+        - name: ARGOCD_RECONCILIATION_TIMEOUT
+          valueFrom:
+            configMapKeyRef:
+              key: timeout.reconciliation
+              name: argocd-cm
+              optional: true
         image: quay.io/argoproj/argocd:latest
         imagePullPolicy: Always
         livenessProbe:
@@ -554,6 +561,8 @@ spec:
           capabilities:
             drop:
             - all
+          readOnlyRootFilesystem: true
+          runAsNonRoot: true
         volumeMounts:
         - mountPath: /app/config/ssh
           name: ssh-known-hosts
@@ -565,6 +574,8 @@ spec:
           name: gpg-keyring
         - mountPath: /app/config/reposerver/tls
           name: argocd-repo-server-tls
+        - mountPath: /tmp
+          name: tmp
       volumes:
       - configMap:
           name: argocd-ssh-known-hosts-cm
@@ -577,6 +588,8 @@ spec:
         name: gpg-keys
       - emptyDir: {}
         name: gpg-keyring
+      - emptyDir: {}
+        name: tmp
       - name: argocd-repo-server-tls
         secret:
           items:
@@ -729,6 +742,13 @@ spec:
         - "20"
         - --operation-processors
         - "10"
+        env:
+        - name: ARGOCD_RECONCILIATION_TIMEOUT
+          valueFrom:
+            configMapKeyRef:
+              key: timeout.reconciliation
+              name: argocd-cm
+              optional: true
         image: quay.io/argoproj/argocd:latest
         imagePullPolicy: Always
         livenessProbe:
diff --git a/pkg/apis/application/v1alpha1/app_project_types.go b/pkg/apis/application/v1alpha1/app_project_types.go
new file mode 100644
index 000000000..3eefef42c
--- /dev/null
+++ b/pkg/apis/application/v1alpha1/app_project_types.go
@@ -0,0 +1,446 @@
+package v1alpha1
+
+import (
+	fmt "fmt"
+	"sort"
+	"strconv"
+	strings "strings"
+
+	"github.com/argoproj/argo-cd/v2/util/git"
+	"github.com/argoproj/argo-cd/v2/util/glob"
+
+	"github.com/google/go-cmp/cmp"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+// AppProjectList is list of AppProject resources
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+type AppProjectList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"`
+	Items           []AppProject `json:"items" protobuf:"bytes,2,rep,name=items"`
+}
+
+// AppProject provides a logical grouping of applications, providing controls for:
+// * where the apps may deploy to (cluster whitelist)
+// * what may be deployed (repository whitelist, resource whitelist/blacklist)
+// * who can access these applications (roles, OIDC group claims bindings)
+// * and what they can do (RBAC policies)
+// * automation access to these roles (JWT tokens)
+// +genclient
+// +genclient:noStatus
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// +kubebuilder:resource:path=appprojects,shortName=appproj;appprojs
+type AppProject struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"`
+	Spec              AppProjectSpec   `json:"spec" protobuf:"bytes,2,opt,name=spec"`
+	Status            AppProjectStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
+}
+
+// AppProjectStatus contains status information for AppProject CRs
+type AppProjectStatus struct {
+	// JWTTokensByRole contains a list of JWT tokens issued for a given role
+	JWTTokensByRole map[string]JWTTokens `json:"jwtTokensByRole,omitempty" protobuf:"bytes,1,opt,name=jwtTokensByRole"`
+}
+
+// GetRoleByName returns the role in a project by the name with its index
+func (p *AppProject) GetRoleByName(name string) (*ProjectRole, int, error) {
+	for i, role := range p.Spec.Roles {
+		if name == role.Name {
+			return &role, i, nil
+		}
+	}
+	return nil, -1, fmt.Errorf("role '%s' does not exist in project '%s'", name, p.Name)
+}
+
+// GetJWTToken looks up the index of a JWTToken in a project by id (new token), if not then by the issue at time (old token)
+func (p *AppProject) GetJWTTokenFromSpec(roleName string, issuedAt int64, id string) (*JWTToken, int, error) {
+	// This is for backward compatibility. In the oder version, JWTTokens are stored under spec.role
+	role, _, err := p.GetRoleByName(roleName)
+	if err != nil {
+		return nil, -1, err
+	}
+
+	if id != "" {
+		for i, token := range role.JWTTokens {
+			if id == token.ID {
+				return &token, i, nil
+			}
+		}
+	}
+
+	if issuedAt != -1 {
+		for i, token := range role.JWTTokens {
+			if issuedAt == token.IssuedAt {
+				return &token, i, nil
+			}
+		}
+	}
+
+	return nil, -1, fmt.Errorf("JWT token for role '%s' issued at '%d' does not exist in project '%s'", role.Name, issuedAt, p.Name)
+}
+
+// GetJWTToken looks up the index of a JWTToken in a project by id (new token), if not then by the issue at time (old token)
+func (p *AppProject) GetJWTToken(roleName string, issuedAt int64, id string) (*JWTToken, int, error) {
+	// This is for newer version, JWTTokens are stored under status
+	if id != "" {
+		for i, token := range p.Status.JWTTokensByRole[roleName].Items {
+			if id == token.ID {
+				return &token, i, nil
+			}
+		}
+
+	}
+
+	if issuedAt != -1 {
+		for i, token := range p.Status.JWTTokensByRole[roleName].Items {
+			if issuedAt == token.IssuedAt {
+				return &token, i, nil
+			}
+		}
+	}
+
+	return nil, -1, fmt.Errorf("JWT token for role '%s' issued at '%d' does not exist in project '%s'", roleName, issuedAt, p.Name)
+}
+
+// RemoveJWTToken removes the specified JWT from an AppProject
+func (p AppProject) RemoveJWTToken(roleIndex int, issuedAt int64, id string) error {
+	roleName := p.Spec.Roles[roleIndex].Name
+	// For backward compatibility
+	_, jwtTokenIndex, err1 := p.GetJWTTokenFromSpec(roleName, issuedAt, id)
+	if err1 == nil {
+		p.Spec.Roles[roleIndex].JWTTokens[jwtTokenIndex] = p.Spec.Roles[roleIndex].JWTTokens[len(p.Spec.Roles[roleIndex].JWTTokens)-1]
+		p.Spec.Roles[roleIndex].JWTTokens = p.Spec.Roles[roleIndex].JWTTokens[:len(p.Spec.Roles[roleIndex].JWTTokens)-1]
+	}
+
+	// New location for storing JWTToken
+	_, jwtTokenIndex, err2 := p.GetJWTToken(roleName, issuedAt, id)
+	if err2 == nil {
+		p.Status.JWTTokensByRole[roleName].Items[jwtTokenIndex] = p.Status.JWTTokensByRole[roleName].Items[len(p.Status.JWTTokensByRole[roleName].Items)-1]
+		p.Status.JWTTokensByRole[roleName] = JWTTokens{Items: p.Status.JWTTokensByRole[roleName].Items[:len(p.Status.JWTTokensByRole[roleName].Items)-1]}
+	}
+
+	if err1 == nil || err2 == nil {
+		//If we find this token from either places, we can say there are no error
+		return nil
+	} else {
+		//If we could not locate this taken from either places, we can return any of the errors
+		return err2
+	}
+}
+
+// TODO: document this method
+func (p *AppProject) ValidateJWTTokenID(roleName string, id string) error {
+	role, _, err := p.GetRoleByName(roleName)
+	if err != nil {
+		return err
+	}
+	if id == "" {
+		return nil
+	}
+	for _, token := range role.JWTTokens {
+		if id == token.ID {
+			return status.Errorf(codes.InvalidArgument, "Token id '%s' has been used. ", id)
+		}
+	}
+	return nil
+}
+
+func (p *AppProject) ValidateProject() error {
+	destKeys := make(map[string]bool)
+	for _, dest := range p.Spec.Destinations {
+		key := fmt.Sprintf("%s/%s", dest.Server, dest.Namespace)
+		if _, ok := destKeys[key]; ok {
+			return status.Errorf(codes.InvalidArgument, "destination '%s' already added", key)
+		}
+		destKeys[key] = true
+	}
+	srcRepos := make(map[string]bool)
+	for _, src := range p.Spec.SourceRepos {
+		if _, ok := srcRepos[src]; ok {
+			return status.Errorf(codes.InvalidArgument, "source repository '%s' already added", src)
+		}
+		srcRepos[src] = true
+	}
+
+	roleNames := make(map[string]bool)
+	for _, role := range p.Spec.Roles {
+		if _, ok := roleNames[role.Name]; ok {
+			return status.Errorf(codes.AlreadyExists, "role '%s' already exists", role.Name)
+		}
+		if err := validateRoleName(role.Name); err != nil {
+			return err
+		}
+		existingPolicies := make(map[string]bool)
+		for _, policy := range role.Policies {
+			if _, ok := existingPolicies[policy]; ok {
+				return status.Errorf(codes.AlreadyExists, "policy '%s' already exists for role '%s'", policy, role.Name)
+			}
+			if err := validatePolicy(p.Name, role.Name, policy); err != nil {
+				return err
+			}
+			existingPolicies[policy] = true
+		}
+		existingGroups := make(map[string]bool)
+		for _, group := range role.Groups {
+			if _, ok := existingGroups[group]; ok {
+				return status.Errorf(codes.AlreadyExists, "group '%s' already exists for role '%s'", group, role.Name)
+			}
+			if err := validateGroupName(group); err != nil {
+				return err
+			}
+			existingGroups[group] = true
+		}
+		roleNames[role.Name] = true
+	}
+
+	if p.Spec.SyncWindows.HasWindows() {
+		existingWindows := make(map[string]bool)
+		for _, window := range p.Spec.SyncWindows {
+			if _, ok := existingWindows[window.Kind+window.Schedule+window.Duration]; ok {
+				return status.Errorf(codes.AlreadyExists, "window '%s':'%s':'%s' already exists, update or edit", window.Kind, window.Schedule, window.Duration)
+			}
+			err := window.Validate()
+			if err != nil {
+				return err
+			}
+			if len(window.Applications) == 0 && len(window.Namespaces) == 0 && len(window.Clusters) == 0 {
+				return status.Errorf(codes.OutOfRange, "window '%s':'%s':'%s' requires one of application, cluster or namespace", window.Kind, window.Schedule, window.Duration)
+			}
+			existingWindows[window.Kind+window.Schedule+window.Duration] = true
+		}
+	}
+
+	return nil
+}
+
+// AddGroupToRole adds an OIDC group to a role
+func (p *AppProject) AddGroupToRole(roleName, group string) (bool, error) {
+	role, roleIndex, err := p.GetRoleByName(roleName)
+	if err != nil {
+		return false, err
+	}
+	for _, roleGroup := range role.Groups {
+		if group == roleGroup {
+			return false, nil
+		}
+	}
+	role.Groups = append(role.Groups, group)
+	p.Spec.Roles[roleIndex] = *role
+	return true, nil
+}
+
+// RemoveGroupFromRole removes an OIDC group from a role
+func (p *AppProject) RemoveGroupFromRole(roleName, group string) (bool, error) {
+	role, roleIndex, err := p.GetRoleByName(roleName)
+	if err != nil {
+		return false, err
+	}
+	for i, roleGroup := range role.Groups {
+		if group == roleGroup {
+			role.Groups = append(role.Groups[:i], role.Groups[i+1:]...)
+			p.Spec.Roles[roleIndex] = *role
+			return true, nil
+		}
+	}
+	return false, nil
+}
+
+// NormalizePolicies normalizes the policies in the project
+func (p *AppProject) NormalizePolicies() {
+	for i, role := range p.Spec.Roles {
+		var normalizedPolicies []string
+		for _, policy := range role.Policies {
+			normalizedPolicies = append(normalizedPolicies, p.normalizePolicy(policy))
+		}
+		p.Spec.Roles[i].Policies = normalizedPolicies
+	}
+}
+
+func (p *AppProject) normalizePolicy(policy string) string {
+	policyComponents := strings.Split(policy, ",")
+	normalizedPolicy := ""
+	for _, component := range policyComponents {
+		if normalizedPolicy == "" {
+			normalizedPolicy = component
+		} else {
+			normalizedPolicy = fmt.Sprintf("%s, %s", normalizedPolicy, strings.Trim(component, " "))
+		}
+	}
+	return normalizedPolicy
+}
+
+// ProjectPoliciesString returns a Casbin formated string of a project's policies for each role
+func (proj *AppProject) ProjectPoliciesString() string {
+	var policies []string
+	for _, role := range proj.Spec.Roles {
+		projectPolicy := fmt.Sprintf("p, proj:%s:%s, projects, get, %s, allow", proj.ObjectMeta.Name, role.Name, proj.ObjectMeta.Name)
+		policies = append(policies, projectPolicy)
+		policies = append(policies, role.Policies...)
+		for _, groupName := range role.Groups {
+			policies = append(policies, fmt.Sprintf("g, %s, proj:%s:%s", groupName, proj.ObjectMeta.Name, role.Name))
+		}
+	}
+	return strings.Join(policies, "\n")
+}
+
+// IsGroupKindPermitted validates if the given resource group/kind is permitted to be deployed in the project
+func (proj AppProject) IsGroupKindPermitted(gk schema.GroupKind, namespaced bool) bool {
+	var isWhiteListed, isBlackListed bool
+	res := metav1.GroupKind{Group: gk.Group, Kind: gk.Kind}
+
+	if namespaced {
+		namespaceWhitelist := proj.Spec.NamespaceResourceWhitelist
+		namespaceBlacklist := proj.Spec.NamespaceResourceBlacklist
+
+		isWhiteListed = namespaceWhitelist == nil || len(namespaceWhitelist) != 0 && isResourceInList(res, namespaceWhitelist)
+		isBlackListed = len(namespaceBlacklist) != 0 && isResourceInList(res, namespaceBlacklist)
+		return isWhiteListed && !isBlackListed
+	}
+
+	clusterWhitelist := proj.Spec.ClusterResourceWhitelist
+	clusterBlacklist := proj.Spec.ClusterResourceBlacklist
+
+	isWhiteListed = len(clusterWhitelist) != 0 && isResourceInList(res, clusterWhitelist)
+	isBlackListed = len(clusterBlacklist) != 0 && isResourceInList(res, clusterBlacklist)
+	return isWhiteListed && !isBlackListed
+}
+
+// IsLiveResourcePermitted returns whether a live resource found in the cluster is permitted by an AppProject
+func (proj AppProject) IsLiveResourcePermitted(un *unstructured.Unstructured, server string) bool {
+	if !proj.IsGroupKindPermitted(un.GroupVersionKind().GroupKind(), un.GetNamespace() != "") {
+		return false
+	}
+	if un.GetNamespace() != "" {
+		return proj.IsDestinationPermitted(ApplicationDestination{Server: server, Namespace: un.GetNamespace()})
+	}
+	return true
+}
+
+// HasFinalizer returns true if a resource finalizer is set on an AppProject
+func (proj AppProject) HasFinalizer() bool {
+	return getFinalizerIndex(proj.ObjectMeta, ResourcesFinalizerName) > -1
+}
+
+// RemoveFinalizer removes a resource finalizer from an AppProject
+func (proj *AppProject) RemoveFinalizer() {
+	setFinalizer(&proj.ObjectMeta, ResourcesFinalizerName, false)
+}
+
+func globMatch(pattern string, val string, separators ...rune) bool {
+	if pattern == "*" {
+		return true
+	}
+	return glob.Match(pattern, val, separators...)
+}
+
+// IsSourcePermitted validates if the provided application's source is a one of the allowed sources for the project.
+func (proj AppProject) IsSourcePermitted(src ApplicationSource) bool {
+	srcNormalized := git.NormalizeGitURL(src.RepoURL)
+	for _, repoURL := range proj.Spec.SourceRepos {
+		normalized := git.NormalizeGitURL(repoURL)
+		if globMatch(normalized, srcNormalized, '/') {
+			return true
+		}
+	}
+	return false
+}
+
+// IsDestinationPermitted validates if the provided application's destination is one of the allowed destinations for the project
+func (proj AppProject) IsDestinationPermitted(dst ApplicationDestination) bool {
+	for _, item := range proj.Spec.Destinations {
+		if globMatch(item.Server, dst.Server) && globMatch(item.Namespace, dst.Namespace) {
+			return true
+		}
+	}
+	return false
+}
+
+// TODO: document this method
+func (proj *AppProject) NormalizeJWTTokens() bool {
+	needNormalize := false
+	for i, role := range proj.Spec.Roles {
+		for j, token := range role.JWTTokens {
+			if token.ID == "" {
+				token.ID = strconv.FormatInt(token.IssuedAt, 10)
+				role.JWTTokens[j] = token
+				needNormalize = true
+			}
+		}
+		proj.Spec.Roles[i] = role
+	}
+	for _, roleTokenEntry := range proj.Status.JWTTokensByRole {
+		for j, token := range roleTokenEntry.Items {
+			if token.ID == "" {
+				token.ID = strconv.FormatInt(token.IssuedAt, 10)
+				roleTokenEntry.Items[j] = token
+				needNormalize = true
+			}
+		}
+	}
+	needSync := syncJWTTokenBetweenStatusAndSpec(proj)
+	return needNormalize || needSync
+}
+
+func syncJWTTokenBetweenStatusAndSpec(proj *AppProject) bool {
+	existingRole := map[string]bool{}
+	needSync := false
+	for roleIndex, role := range proj.Spec.Roles {
+		existingRole[role.Name] = true
+
+		tokensInSpec := role.JWTTokens
+		tokensInStatus := []JWTToken{}
+		if proj.Status.JWTTokensByRole == nil {
+			tokensByRole := make(map[string]JWTTokens)
+			proj.Status.JWTTokensByRole = tokensByRole
+		} else {
+			tokensInStatus = proj.Status.JWTTokensByRole[role.Name].Items
+		}
+		tokens := jwtTokensCombine(tokensInStatus, tokensInSpec)
+
+		sort.Slice(proj.Spec.Roles[roleIndex].JWTTokens, func(i, j int) bool {
+			return proj.Spec.Roles[roleIndex].JWTTokens[i].IssuedAt > proj.Spec.Roles[roleIndex].JWTTokens[j].IssuedAt
+		})
+		sort.Slice(proj.Status.JWTTokensByRole[role.Name].Items, func(i, j int) bool {
+			return proj.Status.JWTTokensByRole[role.Name].Items[i].IssuedAt > proj.Status.JWTTokensByRole[role.Name].Items[j].IssuedAt
+		})
+		if !cmp.Equal(tokens, proj.Spec.Roles[roleIndex].JWTTokens) || !cmp.Equal(tokens, proj.Status.JWTTokensByRole[role.Name].Items) {
+			needSync = true
+		}
+
+		proj.Spec.Roles[roleIndex].JWTTokens = tokens
+		proj.Status.JWTTokensByRole[role.Name] = JWTTokens{Items: tokens}
+	}
+	if proj.Status.JWTTokensByRole != nil {
+		for role := range proj.Status.JWTTokensByRole {
+			if !existingRole[role] {
+				delete(proj.Status.JWTTokensByRole, role)
+				needSync = true
+			}
+		}
+	}
+
+	return needSync
+}
+
+func jwtTokensCombine(tokens1 []JWTToken, tokens2 []JWTToken) []JWTToken {
+	tokensMap := make(map[string]JWTToken)
+	for _, token := range append(tokens1, tokens2...) {
+		tokensMap[token.ID] = token
+	}
+
+	var tokens []JWTToken
+	for _, v := range tokensMap {
+		tokens = append(tokens, v)
+	}
+
+	sort.Slice(tokens, func(i, j int) bool {
+		return tokens[i].IssuedAt > tokens[j].IssuedAt
+	})
+	return tokens
+}
diff --git a/pkg/apis/application/v1alpha1/application_annotations.go b/pkg/apis/application/v1alpha1/application_annotations.go
new file mode 100644
index 000000000..2c5e4ac3f
--- /dev/null
+++ b/pkg/apis/application/v1alpha1/application_annotations.go
@@ -0,0 +1,13 @@
+package v1alpha1
+
+const (
+	// AnnotationKeyRefresh is the annotation key which indicates that app needs to be refreshed. Removed by application controller after app is refreshed.
+	// Might take values 'normal'/'hard'. Value 'hard' means manifest cache and target cluster state cache should be invalidated before refresh.
+	AnnotationKeyRefresh string = "argocd.argoproj.io/refresh"
+
+	// AnnotationKeyManifestGeneratePaths is an annotation that contains a list of semicolon-separated paths in the
+	// manifests repository that affects the manifest generation. Paths might be either relative or absolute. The
+	// absolute path means an absolute path within the repository and the relative path is relative to the application
+	// source path within the repository.
+	AnnotationKeyManifestGeneratePaths = "argocd.argoproj.io/manifest-generate-paths"
+)
diff --git a/pkg/apis/application/v1alpha1/application_defaults.go b/pkg/apis/application/v1alpha1/application_defaults.go
new file mode 100644
index 000000000..2bc9b1bd0
--- /dev/null
+++ b/pkg/apis/application/v1alpha1/application_defaults.go
@@ -0,0 +1,26 @@
+package v1alpha1
+
+import "time"
+
+const (
+	DefaultSyncRetryMaxDuration time.Duration = 180000000000 // 3m0s
+	DefaultSyncRetryDuration    time.Duration = 5000000000   // 5s
+	DefaultSyncRetryFactor                    = int64(2)
+	// ResourcesFinalizerName is the finalizer value which we inject to finalize deletion of an application
+	ResourcesFinalizerName string = "resources-finalizer.argocd.argoproj.io"
+
+	// ForegroundPropagationPolicyFinalizer is the finalizer we inject to delete application with foreground propagation policy
+	ForegroundPropagationPolicyFinalizer string = "resources-finalizer.argocd.argoproj.io/foreground"
+
+	// BackgroundPropagationPolicyFinalizer is the finalizer we inject to delete application with background propagation policy
+	BackgroundPropagationPolicyFinalizer = "resources-finalizer.argocd.argoproj.io/background"
+
+	// DefaultAppProjectName contains name of 'default' app project, which is available in every Argo CD installation
+	DefaultAppProjectName = "default"
+
+	// RevisionHistoryLimit is the max number of successful sync to keep in history
+	RevisionHistoryLimit = 10
+
+	// KubernetesInternalAPIServerAddr is address of the k8s API server when accessing internal to the cluster
+	KubernetesInternalAPIServerAddr = "https://kubernetes.default.svc"
+)
diff --git a/pkg/apis/application/v1alpha1/cluster_constants.go b/pkg/apis/application/v1alpha1/cluster_constants.go
new file mode 100644
index 000000000..eab9c4cda
--- /dev/null
+++ b/pkg/apis/application/v1alpha1/cluster_constants.go
@@ -0,0 +1,57 @@
+package v1alpha1
+
+import (
+	"os"
+	"strconv"
+)
+
+const (
+
+	// EnvVarFakeInClusterConfig is an environment variable to fake an in-cluster RESTConfig using
+	// the current kubectl context (for development purposes)
+	EnvVarFakeInClusterConfig = "ARGOCD_FAKE_IN_CLUSTER"
+
+	// EnvK8sClientQPS is the QPS value used for the kubernetes client (default: 50)
+	EnvK8sClientQPS = "ARGOCD_K8S_CLIENT_QPS"
+
+	// EnvK8sClientBurst is the burst value used for the kubernetes client (default: twice the client QPS)
+	EnvK8sClientBurst = "ARGOCD_K8S_CLIENT_BURST"
+
+	// EnvK8sClientMaxIdleConnections is the number of max idle connections in K8s REST client HTTP transport (default: 500)
+	EnvK8sClientMaxIdleConnections = "ARGOCD_K8S_CLIENT_MAX_IDLE_CONNECTIONS"
+)
+
+// Constants associated with the Cluster API
+var (
+
+	// K8sClientConfigQPS controls the QPS to be used in K8s REST client configs
+	K8sClientConfigQPS float32 = 50
+
+	// K8sClientConfigBurst controls the burst to be used in K8s REST client configs
+	K8sClientConfigBurst int = 100
+
+	// K8sMaxIdleConnections controls the number of max idle connections in K8s REST client HTTP transport
+	K8sMaxIdleConnections = 500
+)
+
+func init() {
+	if envQPS := os.Getenv(EnvK8sClientQPS); envQPS != "" {
+		if qps, err := strconv.ParseFloat(envQPS, 32); err != nil {
+			K8sClientConfigQPS = float32(qps)
+		}
+	}
+	if envBurst := os.Getenv(EnvK8sClientBurst); envBurst != "" {
+		if burst, err := strconv.Atoi(envBurst); err != nil {
+			K8sClientConfigBurst = burst
+		}
+	} else {
+		K8sClientConfigBurst = 2 * int(K8sClientConfigQPS)
+	}
+
+	if envMaxConn := os.Getenv(EnvK8sClientMaxIdleConnections); envMaxConn != "" {
+		if maxConn, err := strconv.Atoi(envMaxConn); err != nil {
+			K8sMaxIdleConnections = maxConn
+		}
+	}
+
+}
diff --git a/pkg/apis/application/v1alpha1/repository_types.go b/pkg/apis/application/v1alpha1/repository_types.go
new file mode 100644
index 000000000..75017de41
--- /dev/null
+++ b/pkg/apis/application/v1alpha1/repository_types.go
@@ -0,0 +1,288 @@
+package v1alpha1
+
+import (
+	"net/url"
+
+	"github.com/argoproj/argo-cd/v2/util/cert"
+	"github.com/argoproj/argo-cd/v2/util/git"
+	"github.com/argoproj/argo-cd/v2/util/helm"
+
+	log "github.com/sirupsen/logrus"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// RepoCreds holds the definition for repository credentials
+type RepoCreds struct {
+	// URL is the URL that this credentials matches to
+	URL string `json:"url" protobuf:"bytes,1,opt,name=url"`
+	// Username for authenticating at the repo server
+	Username string `json:"username,omitempty" protobuf:"bytes,2,opt,name=username"`
+	// Password for authenticating at the repo server
+	Password string `json:"password,omitempty" protobuf:"bytes,3,opt,name=password"`
+	// SSHPrivateKey contains the private key data for authenticating at the repo server using SSH (only Git repos)
+	SSHPrivateKey string `json:"sshPrivateKey,omitempty" protobuf:"bytes,4,opt,name=sshPrivateKey"`
+	// TLSClientCertData specifies the TLS client cert data for authenticating at the repo server
+	TLSClientCertData string `json:"tlsClientCertData,omitempty" protobuf:"bytes,5,opt,name=tlsClientCertData"`
+	// TLSClientCertKey specifies the TLS client cert key for authenticating at the repo server
+	TLSClientCertKey string `json:"tlsClientCertKey,omitempty" protobuf:"bytes,6,opt,name=tlsClientCertKey"`
+	// GithubAppPrivateKey specifies the private key PEM data for authentication via GitHub app
+	GithubAppPrivateKey string `json:"githubAppPrivateKey,omitempty" protobuf:"bytes,7,opt,name=githubAppPrivateKey"`
+	// GithubAppId specifies the Github App ID of the app used to access the repo for GitHub app authentication
+	GithubAppId int64 `json:"githubAppID,omitempty" protobuf:"bytes,8,opt,name=githubAppID"`
+	// GithubAppInstallationId specifies the ID of the installed GitHub App for GitHub app authentication
+	GithubAppInstallationId int64 `json:"githubAppInstallationID,omitempty" protobuf:"bytes,9,opt,name=githubAppInstallationID"`
+	// GithubAppEnterpriseBaseURL specifies the GitHub API URL for GitHub app authentication. If empty will default to https://api.github.com
+	GitHubAppEnterpriseBaseURL string `json:"githubAppEnterpriseBaseUrl,omitempty" protobuf:"bytes,10,opt,name=githubAppEnterpriseBaseUrl"`
+	// EnableOCI specifies whether helm-oci support should be enabled for this repo
+	EnableOCI bool `json:"enableOCI,omitempty" protobuf:"bytes,11,opt,name=enableOCI"`
+	// Type specifies the type of the repoCreds. Can be either "git" or "helm. "git" is assumed if empty or absent.
+	Type string `json:"type,omitempty" protobuf:"bytes,12,opt,name=type"`
+}
+
+// Repository is a repository holding application configurations
+type Repository struct {
+	// Repo contains the URL to the remote repository
+	Repo string `json:"repo" protobuf:"bytes,1,opt,name=repo"`
+	// Username contains the user name used for authenticating at the remote repository
+	Username string `json:"username,omitempty" protobuf:"bytes,2,opt,name=username"`
+	// Password contains the password or PAT used for authenticating at the remote repository
+	Password string `json:"password,omitempty" protobuf:"bytes,3,opt,name=password"`
+	// SSHPrivateKey contains the PEM data for authenticating at the repo server. Only used with Git repos.
+	SSHPrivateKey string `json:"sshPrivateKey,omitempty" protobuf:"bytes,4,opt,name=sshPrivateKey"`
+	// ConnectionState contains information about the current state of connection to the repository server
+	ConnectionState ConnectionState `json:"connectionState,omitempty" protobuf:"bytes,5,opt,name=connectionState"`
+	// InsecureIgnoreHostKey should not be used anymore, Insecure is favoured
+	// Used only for Git repos
+	InsecureIgnoreHostKey bool `json:"insecureIgnoreHostKey,omitempty" protobuf:"bytes,6,opt,name=insecureIgnoreHostKey"`
+	// Insecure specifies whether the connection to the repository ignores any errors when verifying TLS certificates or SSH host keys
+	Insecure bool `json:"insecure,omitempty" protobuf:"bytes,7,opt,name=insecure"`
+	// EnableLFS specifies whether git-lfs support should be enabled for this repo. Only valid for Git repositories.
+	EnableLFS bool `json:"enableLfs,omitempty" protobuf:"bytes,8,opt,name=enableLfs"`
+	// TLSClientCertData contains a certificate in PEM format for authenticating at the repo server
+	TLSClientCertData string `json:"tlsClientCertData,omitempty" protobuf:"bytes,9,opt,name=tlsClientCertData"`
+	// TLSClientCertKey contains a private key in PEM format for authenticating at the repo server
+	TLSClientCertKey string `json:"tlsClientCertKey,omitempty" protobuf:"bytes,10,opt,name=tlsClientCertKey"`
+	// Type specifies the type of the repo. Can be either "git" or "helm. "git" is assumed if empty or absent.
+	Type string `json:"type,omitempty" protobuf:"bytes,11,opt,name=type"`
+	// Name specifies a name to be used for this repo. Only used with Helm repos
+	Name string `json:"name,omitempty" protobuf:"bytes,12,opt,name=name"`
+	// Whether credentials were inherited from a credential set
+	InheritedCreds bool `json:"inheritedCreds,omitempty" protobuf:"bytes,13,opt,name=inheritedCreds"`
+	// EnableOCI specifies whether helm-oci support should be enabled for this repo
+	EnableOCI bool `json:"enableOCI,omitempty" protobuf:"bytes,14,opt,name=enableOCI"`
+	// Github App Private Key PEM data
+	GithubAppPrivateKey string `json:"githubAppPrivateKey,omitempty" protobuf:"bytes,15,opt,name=githubAppPrivateKey"`
+	// GithubAppId specifies the ID of the GitHub app used to access the repo
+	GithubAppId int64 `json:"githubAppID,omitempty" protobuf:"bytes,16,opt,name=githubAppID"`
+	// GithubAppInstallationId specifies the installation ID of the GitHub App used to access the repo
+	GithubAppInstallationId int64 `json:"githubAppInstallationID,omitempty" protobuf:"bytes,17,opt,name=githubAppInstallationID"`
+	// GithubAppEnterpriseBaseURL specifies the base URL of GitHub Enterprise installation. If empty will default to https://api.github.com
+	GitHubAppEnterpriseBaseURL string `json:"githubAppEnterpriseBaseUrl,omitempty" protobuf:"bytes,18,opt,name=githubAppEnterpriseBaseUrl"`
+}
+
+// IsInsecure returns true if the repository has been configured to skip server verification
+func (repo *Repository) IsInsecure() bool {
+	return repo.InsecureIgnoreHostKey || repo.Insecure
+}
+
+// IsLFSEnabled returns true if LFS support is enabled on repository
+func (repo *Repository) IsLFSEnabled() bool {
+	return repo.EnableLFS
+}
+
+// HasCredentials returns true when the repository has been configured with any credentials
+func (m *Repository) HasCredentials() bool {
+	return m.Username != "" || m.Password != "" || m.SSHPrivateKey != "" || m.TLSClientCertData != "" || m.GithubAppPrivateKey != ""
+}
+
+// CopyCredentialsFromRepo copies all credential information from source repository to receiving repository
+func (repo *Repository) CopyCredentialsFromRepo(source *Repository) {
+	if source != nil {
+		if repo.Username == "" {
+			repo.Username = source.Username
+		}
+		if repo.Password == "" {
+			repo.Password = source.Password
+		}
+		if repo.SSHPrivateKey == "" {
+			repo.SSHPrivateKey = source.SSHPrivateKey
+		}
+		if repo.TLSClientCertData == "" {
+			repo.TLSClientCertData = source.TLSClientCertData
+		}
+		if repo.TLSClientCertKey == "" {
+			repo.TLSClientCertKey = source.TLSClientCertKey
+		}
+		if repo.GithubAppPrivateKey == "" {
+			repo.GithubAppPrivateKey = source.GithubAppPrivateKey
+		}
+		if repo.GithubAppId == 0 {
+			repo.GithubAppId = source.GithubAppId
+		}
+		if repo.GithubAppInstallationId == 0 {
+			repo.GithubAppInstallationId = source.GithubAppInstallationId
+		}
+		if repo.GitHubAppEnterpriseBaseURL == "" {
+			repo.GitHubAppEnterpriseBaseURL = source.GitHubAppEnterpriseBaseURL
+		}
+	}
+}
+
+// CopyCredentialsFrom copies credentials from given credential template to receiving repository
+func (repo *Repository) CopyCredentialsFrom(source *RepoCreds) {
+	if source != nil {
+		if repo.Username == "" {
+			repo.Username = source.Username
+		}
+		if repo.Password == "" {
+			repo.Password = source.Password
+		}
+		if repo.SSHPrivateKey == "" {
+			repo.SSHPrivateKey = source.SSHPrivateKey
+		}
+		if repo.TLSClientCertData == "" {
+			repo.TLSClientCertData = source.TLSClientCertData
+		}
+		if repo.TLSClientCertKey == "" {
+			repo.TLSClientCertKey = source.TLSClientCertKey
+		}
+		if repo.GithubAppPrivateKey == "" {
+			repo.GithubAppPrivateKey = source.GithubAppPrivateKey
+		}
+		if repo.GithubAppId == 0 {
+			repo.GithubAppId = source.GithubAppId
+		}
+		if repo.GithubAppInstallationId == 0 {
+			repo.GithubAppInstallationId = source.GithubAppInstallationId
+		}
+		if repo.GitHubAppEnterpriseBaseURL == "" {
+			repo.GitHubAppEnterpriseBaseURL = source.GitHubAppEnterpriseBaseURL
+		}
+	}
+}
+
+// GetGitCreds returns the credentials from a repository configuration used to authenticate at a Git repository
+func (repo *Repository) GetGitCreds() git.Creds {
+	if repo == nil {
+		return git.NopCreds{}
+	}
+	if repo.Username != "" && repo.Password != "" {
+		return git.NewHTTPSCreds(repo.Username, repo.Password, repo.TLSClientCertData, repo.TLSClientCertKey, repo.IsInsecure())
+	}
+	if repo.SSHPrivateKey != "" {
+		return git.NewSSHCreds(repo.SSHPrivateKey, getCAPath(repo.Repo), repo.IsInsecure())
+	}
+	if repo.GithubAppPrivateKey != "" && repo.GithubAppId != 0 && repo.GithubAppInstallationId != 0 {
+		return git.NewGitHubAppCreds(repo.GithubAppId, repo.GithubAppInstallationId, repo.GithubAppPrivateKey, repo.GitHubAppEnterpriseBaseURL, repo.Repo, repo.TLSClientCertData, repo.TLSClientCertKey, repo.IsInsecure())
+	}
+	return git.NopCreds{}
+}
+
+// GetHelmCreds returns the credentials from a repository configuration used to authenticate at a Helm repository
+func (repo *Repository) GetHelmCreds() helm.Creds {
+	return helm.Creds{
+		Username:           repo.Username,
+		Password:           repo.Password,
+		CAPath:             getCAPath(repo.Repo),
+		CertData:           []byte(repo.TLSClientCertData),
+		KeyData:            []byte(repo.TLSClientCertKey),
+		InsecureSkipVerify: repo.Insecure,
+	}
+}
+
+func getCAPath(repoURL string) string {
+	if git.IsHTTPSURL(repoURL) {
+		if parsedURL, err := url.Parse(repoURL); err == nil {
+			if caPath, err := cert.GetCertBundlePathForRepository(parsedURL.Host); err == nil {
+				return caPath
+			} else {
+				log.Warnf("Could not get cert bundle path for host '%s'", parsedURL.Host)
+			}
+		} else {
+			// We don't fail if we cannot parse the URL, but log a warning in that
+			// case. And we execute the command in a verbatim way.
+			log.Warnf("Could not parse repo URL '%s'", repoURL)
+		}
+	}
+	return ""
+}
+
+// CopySettingsFrom copies all repository settings from source to receiver
+func (m *Repository) CopySettingsFrom(source *Repository) {
+	if source != nil {
+		m.EnableLFS = source.EnableLFS
+		m.InsecureIgnoreHostKey = source.InsecureIgnoreHostKey
+		m.Insecure = source.Insecure
+		m.InheritedCreds = source.InheritedCreds
+	}
+}
+
+// Repositories defines a list of Repository configurations
+type Repositories []*Repository
+
+// Filter returns a list of repositories, which only contain items matched by the supplied predicate method
+func (r Repositories) Filter(predicate func(r *Repository) bool) Repositories {
+	var res Repositories
+	for i := range r {
+		repo := r[i]
+		if predicate(repo) {
+			res = append(res, repo)
+		}
+	}
+	return res
+}
+
+// RepositoryList is a collection of Repositories.
+type RepositoryList struct {
+	metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
+	Items           Repositories `json:"items" protobuf:"bytes,2,rep,name=items"`
+}
+
+// RepositoryList is a collection of Repositories.
+type RepoCredsList struct {
+	metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
+	Items           []RepoCreds `json:"items" protobuf:"bytes,2,rep,name=items"`
+}
+
+// A RepositoryCertificate is either SSH known hosts entry or TLS certificate
+type RepositoryCertificate struct {
+	// ServerName specifies the DNS name of the server this certificate is intended for
+	ServerName string `json:"serverName" protobuf:"bytes,1,opt,name=serverName"`
+	// CertType specifies the type of the certificate - currently one of "https" or "ssh"
+	CertType string `json:"certType" protobuf:"bytes,2,opt,name=certType"`
+	// CertSubType specifies the sub type of the cert, i.e. "ssh-rsa"
+	CertSubType string `json:"certSubType" protobuf:"bytes,3,opt,name=certSubType"`
+	// CertData contains the actual certificate data, dependent on the certificate type
+	CertData []byte `json:"certData" protobuf:"bytes,4,opt,name=certData"`
+	// CertInfo will hold additional certificate info, depdendent on the certificate type (e.g. SSH fingerprint, X509 CommonName)
+	CertInfo string `json:"certInfo" protobuf:"bytes,5,opt,name=certInfo"`
+}
+
+// RepositoryCertificateList is a collection of RepositoryCertificates
+type RepositoryCertificateList struct {
+	metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
+	// List of certificates to be processed
+	Items []RepositoryCertificate `json:"items" protobuf:"bytes,2,rep,name=items"`
+}
+
+// GnuPGPublicKey is a representation of a GnuPG public key
+type GnuPGPublicKey struct {
+	// KeyID specifies the key ID, in hexadecimal string format
+	KeyID string `json:"keyID" protobuf:"bytes,1,opt,name=keyID"`
+	// Fingerprint is the fingerprint of the key
+	Fingerprint string `json:"fingerprint,omitempty" protobuf:"bytes,2,opt,name=fingerprint"`
+	// Owner holds the owner identification, e.g. a name and e-mail address
+	Owner string `json:"owner,omitempty" protobuf:"bytes,3,opt,name=owner"`
+	// Trust holds the level of trust assigned to this key
+	Trust string `json:"trust,omitempty" protobuf:"bytes,4,opt,name=trust"`
+	// SubType holds the key's sub type (e.g. rsa4096)
+	SubType string `json:"subType,omitempty" protobuf:"bytes,5,opt,name=subType"`
+	// KeyData holds the raw key data, in base64 encoded format
+	KeyData string `json:"keyData,omitempty" protobuf:"bytes,6,opt,name=keyData"`
+}
+
+// GnuPGPublicKeyList is a collection of GnuPGPublicKey objects
+type GnuPGPublicKeyList struct {
+	metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
+	Items           []GnuPGPublicKey `json:"items" protobuf:"bytes,2,rep,name=items"`
+}
diff --git a/pkg/apis/application/v1alpha1/types.go b/pkg/apis/application/v1alpha1/types.go
index 812be24a6..a3f6b3d9c 100644
--- a/pkg/apis/application/v1alpha1/types.go
+++ b/pkg/apis/application/v1alpha1/types.go
@@ -6,7 +6,6 @@ import (
 	math "math"
 	"net"
 	"net/http"
-	"net/url"
 	"os"
 	"path/filepath"
 	"reflect"
@@ -19,9 +18,7 @@ import (
 	"github.com/argoproj/gitops-engine/pkg/health"
 	synccommon "github.com/argoproj/gitops-engine/pkg/sync/common"
 	"github.com/ghodss/yaml"
-	"github.com/google/go-cmp/cmp"
 	"github.com/robfig/cron"
-	log "github.com/sirupsen/logrus"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 	v1 "k8s.io/api/core/v1"
@@ -34,10 +31,6 @@ import (
 	"k8s.io/client-go/tools/clientcmd"
 	"k8s.io/client-go/tools/clientcmd/api"
 
-	"github.com/argoproj/argo-cd/v2/common"
-	"github.com/argoproj/argo-cd/v2/util/cert"
-	"github.com/argoproj/argo-cd/v2/util/git"
-	"github.com/argoproj/argo-cd/v2/util/glob"
 	"github.com/argoproj/argo-cd/v2/util/helm"
 )
 
@@ -560,12 +553,6 @@ type JWTTokens struct {
 	Items []JWTToken `json:"items,omitempty" protobuf:"bytes,1,opt,name=items"`
 }
 
-// AppProjectStatus contains status information for AppProject CRs
-type AppProjectStatus struct {
-	// JWTTokensByRole contains a list of JWT tokens issued for a given role
-	JWTTokensByRole map[string]JWTTokens `json:"jwtTokensByRole,omitempty" protobuf:"bytes,1,opt,name=jwtTokensByRole"`
-}
-
 // OperationInitiator contains information about the initiator of an operation
 type OperationInitiator struct {
 	// Username contains the name of a user who started operation
@@ -749,9 +736,9 @@ func parseStringToDuration(durationString string) (time.Duration, error) {
 
 // NextRetryAt calculates the earliest time the next retry should be performed on a failing sync
 func (r *RetryStrategy) NextRetryAt(lastAttempt time.Time, retryCounts int64) (time.Time, error) {
-	maxDuration := common.DefaultSyncRetryMaxDuration
-	duration := common.DefaultSyncRetryDuration
-	factor := common.DefaultSyncRetryFactor
+	maxDuration := DefaultSyncRetryMaxDuration
+	duration := DefaultSyncRetryDuration
+	factor := DefaultSyncRetryFactor
 	var err error
 	if r.Backoff != nil {
 		if r.Backoff.Duration != "" {
@@ -1498,478 +1485,6 @@ type ResourceActionParam struct {
 	Default string `json:"default,omitempty" protobuf:"bytes,4,opt,name=default"`
 }
 
-// RepoCreds holds the definition for repository credentials
-type RepoCreds struct {
-	// URL is the URL that this credentials matches to
-	URL string `json:"url" protobuf:"bytes,1,opt,name=url"`
-	// Username for authenticating at the repo server
-	Username string `json:"username,omitempty" protobuf:"bytes,2,opt,name=username"`
-	// Password for authenticating at the repo server
-	Password string `json:"password,omitempty" protobuf:"bytes,3,opt,name=password"`
-	// SSHPrivateKey contains the private key data for authenticating at the repo server using SSH (only Git repos)
-	SSHPrivateKey string `json:"sshPrivateKey,omitempty" protobuf:"bytes,4,opt,name=sshPrivateKey"`
-	// TLSClientCertData specifies the TLS client cert data for authenticating at the repo server
-	TLSClientCertData string `json:"tlsClientCertData,omitempty" protobuf:"bytes,5,opt,name=tlsClientCertData"`
-	// TLSClientCertKey specifies the TLS client cert key for authenticating at the repo server
-	TLSClientCertKey string `json:"tlsClientCertKey,omitempty" protobuf:"bytes,6,opt,name=tlsClientCertKey"`
-	// GithubAppPrivateKey specifies the private key PEM data for authentication via GitHub app
-	GithubAppPrivateKey string `json:"githubAppPrivateKey,omitempty" protobuf:"bytes,7,opt,name=githubAppPrivateKey"`
-	// GithubAppId specifies the Github App ID of the app used to access the repo for GitHub app authentication
-	GithubAppId int64 `json:"githubAppID,omitempty" protobuf:"bytes,8,opt,name=githubAppID"`
-	// GithubAppInstallationId specifies the ID of the installed GitHub App for GitHub app authentication
-	GithubAppInstallationId int64 `json:"githubAppInstallationID,omitempty" protobuf:"bytes,9,opt,name=githubAppInstallationID"`
-	// GithubAppEnterpriseBaseURL specifies the GitHub API URL for GitHub app authentication. If empty will default to https://api.github.com
-	GitHubAppEnterpriseBaseURL string `json:"githubAppEnterpriseBaseUrl,omitempty" protobuf:"bytes,10,opt,name=githubAppEnterpriseBaseUrl"`
-	// EnableOCI specifies whether helm-oci support should be enabled for this repo
-	EnableOCI bool `json:"enableOCI,omitempty" protobuf:"bytes,11,opt,name=enableOCI"`
-	// Type specifies the type of the repoCreds. Can be either "git" or "helm. "git" is assumed if empty or absent.
-	Type string `json:"type,omitempty" protobuf:"bytes,12,opt,name=type"`
-}
-
-// Repository is a repository holding application configurations
-type Repository struct {
-	// Repo contains the URL to the remote repository
-	Repo string `json:"repo" protobuf:"bytes,1,opt,name=repo"`
-	// Username contains the user name used for authenticating at the remote repository
-	Username string `json:"username,omitempty" protobuf:"bytes,2,opt,name=username"`
-	// Password contains the password or PAT used for authenticating at the remote repository
-	Password string `json:"password,omitempty" protobuf:"bytes,3,opt,name=password"`
-	// SSHPrivateKey contains the PEM data for authenticating at the repo server. Only used with Git repos.
-	SSHPrivateKey string `json:"sshPrivateKey,omitempty" protobuf:"bytes,4,opt,name=sshPrivateKey"`
-	// ConnectionState contains information about the current state of connection to the repository server
-	ConnectionState ConnectionState `json:"connectionState,omitempty" protobuf:"bytes,5,opt,name=connectionState"`
-	// InsecureIgnoreHostKey should not be used anymore, Insecure is favoured
-	// Used only for Git repos
-	InsecureIgnoreHostKey bool `json:"insecureIgnoreHostKey,omitempty" protobuf:"bytes,6,opt,name=insecureIgnoreHostKey"`
-	// Insecure specifies whether the connection to the repository ignores any errors when verifying TLS certificates or SSH host keys
-	Insecure bool `json:"insecure,omitempty" protobuf:"bytes,7,opt,name=insecure"`
-	// EnableLFS specifies whether git-lfs support should be enabled for this repo. Only valid for Git repositories.
-	EnableLFS bool `json:"enableLfs,omitempty" protobuf:"bytes,8,opt,name=enableLfs"`
-	// TLSClientCertData contains a certificate in PEM format for authenticating at the repo server
-	TLSClientCertData string `json:"tlsClientCertData,omitempty" protobuf:"bytes,9,opt,name=tlsClientCertData"`
-	// TLSClientCertKey contains a private key in PEM format for authenticating at the repo server
-	TLSClientCertKey string `json:"tlsClientCertKey,omitempty" protobuf:"bytes,10,opt,name=tlsClientCertKey"`
-	// Type specifies the type of the repo. Can be either "git" or "helm. "git" is assumed if empty or absent.
-	Type string `json:"type,omitempty" protobuf:"bytes,11,opt,name=type"`
-	// Name specifies a name to be used for this repo. Only used with Helm repos
-	Name string `json:"name,omitempty" protobuf:"bytes,12,opt,name=name"`
-	// Whether credentials were inherited from a credential set
-	InheritedCreds bool `json:"inheritedCreds,omitempty" protobuf:"bytes,13,opt,name=inheritedCreds"`
-	// EnableOCI specifies whether helm-oci support should be enabled for this repo
-	EnableOCI bool `json:"enableOCI,omitempty" protobuf:"bytes,14,opt,name=enableOCI"`
-	// Github App Private Key PEM data
-	GithubAppPrivateKey string `json:"githubAppPrivateKey,omitempty" protobuf:"bytes,15,opt,name=githubAppPrivateKey"`
-	// GithubAppId specifies the ID of the GitHub app used to access the repo
-	GithubAppId int64 `json:"githubAppID,omitempty" protobuf:"bytes,16,opt,name=githubAppID"`
-	// GithubAppInstallationId specifies the installation ID of the GitHub App used to access the repo
-	GithubAppInstallationId int64 `json:"githubAppInstallationID,omitempty" protobuf:"bytes,17,opt,name=githubAppInstallationID"`
-	// GithubAppEnterpriseBaseURL specifies the base URL of GitHub Enterprise installation. If empty will default to https://api.github.com
-	GitHubAppEnterpriseBaseURL string `json:"githubAppEnterpriseBaseUrl,omitempty" protobuf:"bytes,18,opt,name=githubAppEnterpriseBaseUrl"`
-}
-
-// IsInsecure returns true if the repository has been configured to skip server verification
-func (repo *Repository) IsInsecure() bool {
-	return repo.InsecureIgnoreHostKey || repo.Insecure
-}
-
-// IsLFSEnabled returns true if LFS support is enabled on repository
-func (repo *Repository) IsLFSEnabled() bool {
-	return repo.EnableLFS
-}
-
-// HasCredentials returns true when the repository has been configured with any credentials
-func (m *Repository) HasCredentials() bool {
-	return m.Username != "" || m.Password != "" || m.SSHPrivateKey != "" || m.TLSClientCertData != "" || m.GithubAppPrivateKey != ""
-}
-
-// CopyCredentialsFromRepo copies all credential information from source repository to receiving repository
-func (repo *Repository) CopyCredentialsFromRepo(source *Repository) {
-	if source != nil {
-		if repo.Username == "" {
-			repo.Username = source.Username
-		}
-		if repo.Password == "" {
-			repo.Password = source.Password
-		}
-		if repo.SSHPrivateKey == "" {
-			repo.SSHPrivateKey = source.SSHPrivateKey
-		}
-		if repo.TLSClientCertData == "" {
-			repo.TLSClientCertData = source.TLSClientCertData
-		}
-		if repo.TLSClientCertKey == "" {
-			repo.TLSClientCertKey = source.TLSClientCertKey
-		}
-		if repo.GithubAppPrivateKey == "" {
-			repo.GithubAppPrivateKey = source.GithubAppPrivateKey
-		}
-		if repo.GithubAppId == 0 {
-			repo.GithubAppId = source.GithubAppId
-		}
-		if repo.GithubAppInstallationId == 0 {
-			repo.GithubAppInstallationId = source.GithubAppInstallationId
-		}
-		if repo.GitHubAppEnterpriseBaseURL == "" {
-			repo.GitHubAppEnterpriseBaseURL = source.GitHubAppEnterpriseBaseURL
-		}
-	}
-}
-
-// CopyCredentialsFrom copies credentials from given credential template to receiving repository
-func (repo *Repository) CopyCredentialsFrom(source *RepoCreds) {
-	if source != nil {
-		if repo.Username == "" {
-			repo.Username = source.Username
-		}
-		if repo.Password == "" {
-			repo.Password = source.Password
-		}
-		if repo.SSHPrivateKey == "" {
-			repo.SSHPrivateKey = source.SSHPrivateKey
-		}
-		if repo.TLSClientCertData == "" {
-			repo.TLSClientCertData = source.TLSClientCertData
-		}
-		if repo.TLSClientCertKey == "" {
-			repo.TLSClientCertKey = source.TLSClientCertKey
-		}
-		if repo.GithubAppPrivateKey == "" {
-			repo.GithubAppPrivateKey = source.GithubAppPrivateKey
-		}
-		if repo.GithubAppId == 0 {
-			repo.GithubAppId = source.GithubAppId
-		}
-		if repo.GithubAppInstallationId == 0 {
-			repo.GithubAppInstallationId = source.GithubAppInstallationId
-		}
-		if repo.GitHubAppEnterpriseBaseURL == "" {
-			repo.GitHubAppEnterpriseBaseURL = source.GitHubAppEnterpriseBaseURL
-		}
-	}
-}
-
-// GetGitCreds returns the credentials from a repository configuration used to authenticate at a Git repository
-func (repo *Repository) GetGitCreds() git.Creds {
-	if repo == nil {
-		return git.NopCreds{}
-	}
-	if repo.Username != "" && repo.Password != "" {
-		return git.NewHTTPSCreds(repo.Username, repo.Password, repo.TLSClientCertData, repo.TLSClientCertKey, repo.IsInsecure())
-	}
-	if repo.SSHPrivateKey != "" {
-		return git.NewSSHCreds(repo.SSHPrivateKey, getCAPath(repo.Repo), repo.IsInsecure())
-	}
-	if repo.GithubAppPrivateKey != "" && repo.GithubAppId != 0 && repo.GithubAppInstallationId != 0 {
-		return git.NewGitHubAppCreds(repo.GithubAppId, repo.GithubAppInstallationId, repo.GithubAppPrivateKey, repo.GitHubAppEnterpriseBaseURL, repo.Repo, repo.TLSClientCertData, repo.TLSClientCertKey, repo.IsInsecure())
-	}
-	return git.NopCreds{}
-}
-
-// GetHelmCreds returns the credentials from a repository configuration used to authenticate at a Helm repository
-func (repo *Repository) GetHelmCreds() helm.Creds {
-	return helm.Creds{
-		Username:           repo.Username,
-		Password:           repo.Password,
-		CAPath:             getCAPath(repo.Repo),
-		CertData:           []byte(repo.TLSClientCertData),
-		KeyData:            []byte(repo.TLSClientCertKey),
-		InsecureSkipVerify: repo.Insecure,
-	}
-}
-
-func getCAPath(repoURL string) string {
-	if git.IsHTTPSURL(repoURL) {
-		if parsedURL, err := url.Parse(repoURL); err == nil {
-			if caPath, err := cert.GetCertBundlePathForRepository(parsedURL.Host); err == nil {
-				return caPath
-			} else {
-				log.Warnf("Could not get cert bundle path for host '%s'", parsedURL.Host)
-			}
-		} else {
-			// We don't fail if we cannot parse the URL, but log a warning in that
-			// case. And we execute the command in a verbatim way.
-			log.Warnf("Could not parse repo URL '%s'", repoURL)
-		}
-	}
-	return ""
-}
-
-// CopySettingsFrom copies all repository settings from source to receiver
-func (m *Repository) CopySettingsFrom(source *Repository) {
-	if source != nil {
-		m.EnableLFS = source.EnableLFS
-		m.InsecureIgnoreHostKey = source.InsecureIgnoreHostKey
-		m.Insecure = source.Insecure
-		m.InheritedCreds = source.InheritedCreds
-	}
-}
-
-// Repositories defines a list of Repository configurations
-type Repositories []*Repository
-
-// Filter returns a list of repositories, which only contain items matched by the supplied predicate method
-func (r Repositories) Filter(predicate func(r *Repository) bool) Repositories {
-	var res Repositories
-	for i := range r {
-		repo := r[i]
-		if predicate(repo) {
-			res = append(res, repo)
-		}
-	}
-	return res
-}
-
-// RepositoryList is a collection of Repositories.
-type RepositoryList struct {
-	metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
-	Items           Repositories `json:"items" protobuf:"bytes,2,rep,name=items"`
-}
-
-// RepositoryList is a collection of Repositories.
-type RepoCredsList struct {
-	metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
-	Items           []RepoCreds `json:"items" protobuf:"bytes,2,rep,name=items"`
-}
-
-// A RepositoryCertificate is either SSH known hosts entry or TLS certificate
-type RepositoryCertificate struct {
-	// ServerName specifies the DNS name of the server this certificate is intended for
-	ServerName string `json:"serverName" protobuf:"bytes,1,opt,name=serverName"`
-	// CertType specifies the type of the certificate - currently one of "https" or "ssh"
-	CertType string `json:"certType" protobuf:"bytes,2,opt,name=certType"`
-	// CertSubType specifies the sub type of the cert, i.e. "ssh-rsa"
-	CertSubType string `json:"certSubType" protobuf:"bytes,3,opt,name=certSubType"`
-	// CertData contains the actual certificate data, dependent on the certificate type
-	CertData []byte `json:"certData" protobuf:"bytes,4,opt,name=certData"`
-	// CertInfo will hold additional certificate info, depdendent on the certificate type (e.g. SSH fingerprint, X509 CommonName)
-	CertInfo string `json:"certInfo" protobuf:"bytes,5,opt,name=certInfo"`
-}
-
-// RepositoryCertificateList is a collection of RepositoryCertificates
-type RepositoryCertificateList struct {
-	metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
-	// List of certificates to be processed
-	Items []RepositoryCertificate `json:"items" protobuf:"bytes,2,rep,name=items"`
-}
-
-// GnuPGPublicKey is a representation of a GnuPG public key
-type GnuPGPublicKey struct {
-	// KeyID specifies the key ID, in hexadecimal string format
-	KeyID string `json:"keyID" protobuf:"bytes,1,opt,name=keyID"`
-	// Fingerprint is the fingerprint of the key
-	Fingerprint string `json:"fingerprint,omitempty" protobuf:"bytes,2,opt,name=fingerprint"`
-	// Owner holds the owner identification, e.g. a name and e-mail address
-	Owner string `json:"owner,omitempty" protobuf:"bytes,3,opt,name=owner"`
-	// Trust holds the level of trust assigned to this key
-	Trust string `json:"trust,omitempty" protobuf:"bytes,4,opt,name=trust"`
-	// SubType holds the key's sub type (e.g. rsa4096)
-	SubType string `json:"subType,omitempty" protobuf:"bytes,5,opt,name=subType"`
-	// KeyData holds the raw key data, in base64 encoded format
-	KeyData string `json:"keyData,omitempty" protobuf:"bytes,6,opt,name=keyData"`
-}
-
-// GnuPGPublicKeyList is a collection of GnuPGPublicKey objects
-type GnuPGPublicKeyList struct {
-	metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
-	Items           []GnuPGPublicKey `json:"items" protobuf:"bytes,2,rep,name=items"`
-}
-
-// AppProjectList is list of AppProject resources
-// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
-type AppProjectList struct {
-	metav1.TypeMeta `json:",inline"`
-	metav1.ListMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"`
-	Items           []AppProject `json:"items" protobuf:"bytes,2,rep,name=items"`
-}
-
-// AppProject provides a logical grouping of applications, providing controls for:
-// * where the apps may deploy to (cluster whitelist)
-// * what may be deployed (repository whitelist, resource whitelist/blacklist)
-// * who can access these applications (roles, OIDC group claims bindings)
-// * and what they can do (RBAC policies)
-// * automation access to these roles (JWT tokens)
-// +genclient
-// +genclient:noStatus
-// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
-// +kubebuilder:resource:path=appprojects,shortName=appproj;appprojs
-type AppProject struct {
-	metav1.TypeMeta   `json:",inline"`
-	metav1.ObjectMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"`
-	Spec              AppProjectSpec   `json:"spec" protobuf:"bytes,2,opt,name=spec"`
-	Status            AppProjectStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
-}
-
-// GetRoleByName returns the role in a project by the name with its index
-func (p *AppProject) GetRoleByName(name string) (*ProjectRole, int, error) {
-	for i, role := range p.Spec.Roles {
-		if name == role.Name {
-			return &role, i, nil
-		}
-	}
-	return nil, -1, fmt.Errorf("role '%s' does not exist in project '%s'", name, p.Name)
-}
-
-// GetJWTToken looks up the index of a JWTToken in a project by id (new token), if not then by the issue at time (old token)
-func (p *AppProject) GetJWTTokenFromSpec(roleName string, issuedAt int64, id string) (*JWTToken, int, error) {
-	// This is for backward compatibility. In the oder version, JWTTokens are stored under spec.role
-	role, _, err := p.GetRoleByName(roleName)
-	if err != nil {
-		return nil, -1, err
-	}
-
-	if id != "" {
-		for i, token := range role.JWTTokens {
-			if id == token.ID {
-				return &token, i, nil
-			}
-		}
-	}
-
-	if issuedAt != -1 {
-		for i, token := range role.JWTTokens {
-			if issuedAt == token.IssuedAt {
-				return &token, i, nil
-			}
-		}
-	}
-
-	return nil, -1, fmt.Errorf("JWT token for role '%s' issued at '%d' does not exist in project '%s'", role.Name, issuedAt, p.Name)
-}
-
-// GetJWTToken looks up the index of a JWTToken in a project by id (new token), if not then by the issue at time (old token)
-func (p *AppProject) GetJWTToken(roleName string, issuedAt int64, id string) (*JWTToken, int, error) {
-	// This is for newer version, JWTTokens are stored under status
-	if id != "" {
-		for i, token := range p.Status.JWTTokensByRole[roleName].Items {
-			if id == token.ID {
-				return &token, i, nil
-			}
-		}
-
-	}
-
-	if issuedAt != -1 {
-		for i, token := range p.Status.JWTTokensByRole[roleName].Items {
-			if issuedAt == token.IssuedAt {
-				return &token, i, nil
-			}
-		}
-	}
-
-	return nil, -1, fmt.Errorf("JWT token for role '%s' issued at '%d' does not exist in project '%s'", roleName, issuedAt, p.Name)
-}
-
-// RemoveJWTToken removes the specified JWT from an AppProject
-func (p AppProject) RemoveJWTToken(roleIndex int, issuedAt int64, id string) error {
-	roleName := p.Spec.Roles[roleIndex].Name
-	// For backward compatibility
-	_, jwtTokenIndex, err1 := p.GetJWTTokenFromSpec(roleName, issuedAt, id)
-	if err1 == nil {
-		p.Spec.Roles[roleIndex].JWTTokens[jwtTokenIndex] = p.Spec.Roles[roleIndex].JWTTokens[len(p.Spec.Roles[roleIndex].JWTTokens)-1]
-		p.Spec.Roles[roleIndex].JWTTokens = p.Spec.Roles[roleIndex].JWTTokens[:len(p.Spec.Roles[roleIndex].JWTTokens)-1]
-	}
-
-	// New location for storing JWTToken
-	_, jwtTokenIndex, err2 := p.GetJWTToken(roleName, issuedAt, id)
-	if err2 == nil {
-		p.Status.JWTTokensByRole[roleName].Items[jwtTokenIndex] = p.Status.JWTTokensByRole[roleName].Items[len(p.Status.JWTTokensByRole[roleName].Items)-1]
-		p.Status.JWTTokensByRole[roleName] = JWTTokens{Items: p.Status.JWTTokensByRole[roleName].Items[:len(p.Status.JWTTokensByRole[roleName].Items)-1]}
-	}
-
-	if err1 == nil || err2 == nil {
-		//If we find this token from either places, we can say there are no error
-		return nil
-	} else {
-		//If we could not locate this taken from either places, we can return any of the errors
-		return err2
-	}
-}
-
-// TODO: document this method
-func (p *AppProject) ValidateJWTTokenID(roleName string, id string) error {
-	role, _, err := p.GetRoleByName(roleName)
-	if err != nil {
-		return err
-	}
-	if id == "" {
-		return nil
-	}
-	for _, token := range role.JWTTokens {
-		if id == token.ID {
-			return status.Errorf(codes.InvalidArgument, "Token id '%s' has been used. ", id)
-		}
-	}
-	return nil
-}
-
-func (p *AppProject) ValidateProject() error {
-	destKeys := make(map[string]bool)
-	for _, dest := range p.Spec.Destinations {
-		key := fmt.Sprintf("%s/%s", dest.Server, dest.Namespace)
-		if _, ok := destKeys[key]; ok {
-			return status.Errorf(codes.InvalidArgument, "destination '%s' already added", key)
-		}
-		destKeys[key] = true
-	}
-	srcRepos := make(map[string]bool)
-	for _, src := range p.Spec.SourceRepos {
-		if _, ok := srcRepos[src]; ok {
-			return status.Errorf(codes.InvalidArgument, "source repository '%s' already added", src)
-		}
-		srcRepos[src] = true
-	}
-
-	roleNames := make(map[string]bool)
-	for _, role := range p.Spec.Roles {
-		if _, ok := roleNames[role.Name]; ok {
-			return status.Errorf(codes.AlreadyExists, "role '%s' already exists", role.Name)
-		}
-		if err := validateRoleName(role.Name); err != nil {
-			return err
-		}
-		existingPolicies := make(map[string]bool)
-		for _, policy := range role.Policies {
-			if _, ok := existingPolicies[policy]; ok {
-				return status.Errorf(codes.AlreadyExists, "policy '%s' already exists for role '%s'", policy, role.Name)
-			}
-			if err := validatePolicy(p.Name, role.Name, policy); err != nil {
-				return err
-			}
-			existingPolicies[policy] = true
-		}
-		existingGroups := make(map[string]bool)
-		for _, group := range role.Groups {
-			if _, ok := existingGroups[group]; ok {
-				return status.Errorf(codes.AlreadyExists, "group '%s' already exists for role '%s'", group, role.Name)
-			}
-			if err := validateGroupName(group); err != nil {
-				return err
-			}
-			existingGroups[group] = true
-		}
-		roleNames[role.Name] = true
-	}
-
-	if p.Spec.SyncWindows.HasWindows() {
-		existingWindows := make(map[string]bool)
-		for _, window := range p.Spec.SyncWindows {
-			if _, ok := existingWindows[window.Kind+window.Schedule+window.Duration]; ok {
-				return status.Errorf(codes.AlreadyExists, "window '%s':'%s':'%s' already exists, update or edit", window.Kind, window.Schedule, window.Duration)
-			}
-			err := window.Validate()
-			if err != nil {
-				return err
-			}
-			if len(window.Applications) == 0 && len(window.Namespaces) == 0 && len(window.Clusters) == 0 {
-				return status.Errorf(codes.OutOfRange, "window '%s':'%s':'%s' requires one of application, cluster or namespace", window.Kind, window.Schedule, window.Duration)
-			}
-			existingWindows[window.Kind+window.Schedule+window.Duration] = true
-		}
-	}
-
-	return nil
-}
-
 // TODO: refactor to use rbacpolicy.ActionGet, rbacpolicy.ActionCreate, without import cycle
 var validActions = map[string]bool{
 	"get":      true,
@@ -2053,62 +1568,6 @@ func validateGroupName(name string) error {
 	return nil
 }
 
-// AddGroupToRole adds an OIDC group to a role
-func (p *AppProject) AddGroupToRole(roleName, group string) (bool, error) {
-	role, roleIndex, err := p.GetRoleByName(roleName)
-	if err != nil {
-		return false, err
-	}
-	for _, roleGroup := range role.Groups {
-		if group == roleGroup {
-			return false, nil
-		}
-	}
-	role.Groups = append(role.Groups, group)
-	p.Spec.Roles[roleIndex] = *role
-	return true, nil
-}
-
-// RemoveGroupFromRole removes an OIDC group from a role
-func (p *AppProject) RemoveGroupFromRole(roleName, group string) (bool, error) {
-	role, roleIndex, err := p.GetRoleByName(roleName)
-	if err != nil {
-		return false, err
-	}
-	for i, roleGroup := range role.Groups {
-		if group == roleGroup {
-			role.Groups = append(role.Groups[:i], role.Groups[i+1:]...)
-			p.Spec.Roles[roleIndex] = *role
-			return true, nil
-		}
-	}
-	return false, nil
-}
-
-// NormalizePolicies normalizes the policies in the project
-func (p *AppProject) NormalizePolicies() {
-	for i, role := range p.Spec.Roles {
-		var normalizedPolicies []string
-		for _, policy := range role.Policies {
-			normalizedPolicies = append(normalizedPolicies, p.normalizePolicy(policy))
-		}
-		p.Spec.Roles[i].Policies = normalizedPolicies
-	}
-}
-
-func (p *AppProject) normalizePolicy(policy string) string {
-	policyComponents := strings.Split(policy, ",")
-	normalizedPolicy := ""
-	for _, component := range policyComponents {
-		if normalizedPolicy == "" {
-			normalizedPolicy = component
-		} else {
-			normalizedPolicy = fmt.Sprintf("%s, %s", normalizedPolicy, strings.Trim(component, " "))
-		}
-	}
-	return normalizedPolicy
-}
-
 // OrphanedResourcesMonitorSettings holds settings of orphaned resources monitoring
 type OrphanedResourcesMonitorSettings struct {
 	// Warn indicates if warning condition should be created for apps which have orphaned resources
@@ -2524,20 +1983,6 @@ type KustomizeOptions struct {
 	BinaryPath string `protobuf:"bytes,2,opt,name=binaryPath"`
 }
 
-// ProjectPoliciesString returns a Casbin formated string of a project's policies for each role
-func (proj *AppProject) ProjectPoliciesString() string {
-	var policies []string
-	for _, role := range proj.Spec.Roles {
-		projectPolicy := fmt.Sprintf("p, proj:%s:%s, projects, get, %s, allow", proj.ObjectMeta.Name, role.Name, proj.ObjectMeta.Name)
-		policies = append(policies, projectPolicy)
-		policies = append(policies, role.Policies...)
-		for _, groupName := range role.Groups {
-			policies = append(policies, fmt.Sprintf("g, %s, proj:%s:%s", groupName, proj.ObjectMeta.Name, role.Name))
-		}
-	}
-	return strings.Join(policies, "\n")
-}
-
 // CascadedDeletion indicates if the deletion finalizer is set and controller should delete the application and it's cascaded resources
 func (app *Application) CascadedDeletion() bool {
 	for _, finalizer := range app.ObjectMeta.Finalizers {
@@ -2556,7 +2001,7 @@ func (app *Application) IsRefreshRequested() (RefreshType, bool) {
 		return refreshType, false
 	}
 
-	typeStr, ok := annotations[common.AnnotationKeyRefresh]
+	typeStr, ok := annotations[AnnotationKeyRefresh]
 	if !ok {
 		return refreshType, false
 	}
@@ -2589,11 +2034,11 @@ func (app *Application) UnSetCascadedDeletion() {
 
 func isPropagationPolicyFinalizer(finalizer string) bool {
 	switch finalizer {
-	case common.ResourcesFinalizerName:
+	case ResourcesFinalizerName:
 		return true
-	case common.ForegroundPropagationPolicyFinalizer:
+	case ForegroundPropagationPolicyFinalizer:
 		return true
-	case common.BackgroundPropagationPolicyFinalizer:
+	case BackgroundPropagationPolicyFinalizer:
 		return true
 	default:
 		return false
@@ -2736,7 +2181,7 @@ func (dest ApplicationDestination) Equals(other ApplicationDestination) bool {
 // GetProject returns the application's project. This is preferred over spec.Project which may be empty
 func (spec ApplicationSpec) GetProject() string {
 	if spec.Project == "" {
-		return common.DefaultAppProjectName
+		return DefaultAppProjectName
 	}
 	return spec.Project
 }
@@ -2746,7 +2191,7 @@ func (spec ApplicationSpec) GetRevisionHistoryLimit() int {
 	if spec.RevisionHistoryLimit != nil {
 		return int(*spec.RevisionHistoryLimit)
 	}
-	return common.RevisionHistoryLimit
+	return RevisionHistoryLimit
 }
 
 func isResourceInList(res metav1.GroupKind, list []metav1.GroupKind) bool {
@@ -2762,39 +2207,6 @@ func isResourceInList(res metav1.GroupKind, list []metav1.GroupKind) bool {
 	return false
 }
 
-// IsGroupKindPermitted validates if the given resource group/kind is permitted to be deployed in the project
-func (proj AppProject) IsGroupKindPermitted(gk schema.GroupKind, namespaced bool) bool {
-	var isWhiteListed, isBlackListed bool
-	res := metav1.GroupKind{Group: gk.Group, Kind: gk.Kind}
-
-	if namespaced {
-		namespaceWhitelist := proj.Spec.NamespaceResourceWhitelist
-		namespaceBlacklist := proj.Spec.NamespaceResourceBlacklist
-
-		isWhiteListed = namespaceWhitelist == nil || len(namespaceWhitelist) != 0 && isResourceInList(res, namespaceWhitelist)
-		isBlackListed = len(namespaceBlacklist) != 0 && isResourceInList(res, namespaceBlacklist)
-		return isWhiteListed && !isBlackListed
-	}
-
-	clusterWhitelist := proj.Spec.ClusterResourceWhitelist
-	clusterBlacklist := proj.Spec.ClusterResourceBlacklist
-
-	isWhiteListed = len(clusterWhitelist) != 0 && isResourceInList(res, clusterWhitelist)
-	isBlackListed = len(clusterBlacklist) != 0 && isResourceInList(res, clusterBlacklist)
-	return isWhiteListed && !isBlackListed
-}
-
-// IsLiveResourcePermitted returns whether a live resource found in the cluster is permitted by an AppProject
-func (proj AppProject) IsLiveResourcePermitted(un *unstructured.Unstructured, server string) bool {
-	if !proj.IsGroupKindPermitted(un.GroupVersionKind().GroupKind(), un.GetNamespace() != "") {
-		return false
-	}
-	if un.GetNamespace() != "" {
-		return proj.IsDestinationPermitted(ApplicationDestination{Server: server, Namespace: un.GetNamespace()})
-	}
-	return true
-}
-
 // getFinalizerIndex returns finalizer index in the list of object finalizers or -1 if finalizer does not exist
 func getFinalizerIndex(meta metav1.ObjectMeta, name string) int {
 	for i, finalizer := range meta.Finalizers {
@@ -2818,49 +2230,10 @@ func setFinalizer(meta *metav1.ObjectMeta, name string, exist bool) {
 	}
 }
 
-// HasFinalizer returns true if a resource finalizer is set on an AppProject
-func (proj AppProject) HasFinalizer() bool {
-	return getFinalizerIndex(proj.ObjectMeta, common.ResourcesFinalizerName) > -1
-}
-
-// RemoveFinalizer removes a resource finalizer from an AppProject
-func (proj *AppProject) RemoveFinalizer() {
-	setFinalizer(&proj.ObjectMeta, common.ResourcesFinalizerName, false)
-}
-
-func globMatch(pattern string, val string, separators ...rune) bool {
-	if pattern == "*" {
-		return true
-	}
-	return glob.Match(pattern, val, separators...)
-}
-
-// IsSourcePermitted validates if the provided application's source is a one of the allowed sources for the project.
-func (proj AppProject) IsSourcePermitted(src ApplicationSource) bool {
-	srcNormalized := git.NormalizeGitURL(src.RepoURL)
-	for _, repoURL := range proj.Spec.SourceRepos {
-		normalized := git.NormalizeGitURL(repoURL)
-		if globMatch(normalized, srcNormalized, '/') {
-			return true
-		}
-	}
-	return false
-}
-
-// IsDestinationPermitted validates if the provided application's destination is one of the allowed destinations for the project
-func (proj AppProject) IsDestinationPermitted(dst ApplicationDestination) bool {
-	for _, item := range proj.Spec.Destinations {
-		if globMatch(item.Server, dst.Server) && globMatch(item.Namespace, dst.Namespace) {
-			return true
-		}
-	}
-	return false
-}
-
 // SetK8SConfigDefaults sets Kubernetes REST config default settings
 func SetK8SConfigDefaults(config *rest.Config) error {
-	config.QPS = common.K8sClientConfigQPS
-	config.Burst = common.K8sClientConfigBurst
+	config.QPS = K8sClientConfigQPS
+	config.Burst = K8sClientConfigBurst
 	tlsConfig, err := rest.TLSConfigFor(config)
 	if err != nil {
 		return err
@@ -2874,9 +2247,9 @@ func SetK8SConfigDefaults(config *rest.Config) error {
 		Proxy:               http.ProxyFromEnvironment,
 		TLSHandshakeTimeout: 10 * time.Second,
 		TLSClientConfig:     tlsConfig,
-		MaxIdleConns:        common.K8sMaxIdleConnections,
-		MaxIdleConnsPerHost: common.K8sMaxIdleConnections,
-		MaxConnsPerHost:     common.K8sMaxIdleConnections,
+		MaxIdleConns:        K8sMaxIdleConnections,
+		MaxIdleConnsPerHost: K8sMaxIdleConnections,
+		MaxConnsPerHost:     K8sMaxIdleConnections,
 		DialContext:         dial,
 		DisableCompression:  config.DisableCompression,
 	})
@@ -2898,14 +2271,14 @@ func SetK8SConfigDefaults(config *rest.Config) error {
 func (c *Cluster) RawRestConfig() *rest.Config {
 	var config *rest.Config
 	var err error
-	if c.Server == common.KubernetesInternalAPIServerAddr && os.Getenv(common.EnvVarFakeInClusterConfig) == "true" {
+	if c.Server == KubernetesInternalAPIServerAddr && os.Getenv(EnvVarFakeInClusterConfig) == "true" {
 		conf, exists := os.LookupEnv("KUBECONFIG")
 		if exists {
 			config, err = clientcmd.BuildConfigFromFlags("", conf)
 		} else {
 			config, err = clientcmd.BuildConfigFromFlags("", filepath.Join(os.Getenv("HOME"), ".kube", "config"))
 		}
-	} else if c.Server == common.KubernetesInternalAPIServerAddr && c.Config.Username == "" && c.Config.Password == "" && c.Config.BearerToken == "" {
+	} else if c.Server == KubernetesInternalAPIServerAddr && c.Config.Username == "" && c.Config.Password == "" && c.Config.BearerToken == "" {
 		config, err = rest.InClusterConfig()
 	} else {
 		tlsClientConfig := rest.TLSClientConfig{
@@ -3020,87 +2393,3 @@ func (d *ApplicationDestination) MarshalJSON() ([]byte, error) {
 	}
 	return json.Marshal(&struct{ *Alias }{Alias: (*Alias)(dest)})
 }
-
-// TODO: document this method
-func (proj *AppProject) NormalizeJWTTokens() bool {
-	needNormalize := false
-	for i, role := range proj.Spec.Roles {
-		for j, token := range role.JWTTokens {
-			if token.ID == "" {
-				token.ID = strconv.FormatInt(token.IssuedAt, 10)
-				role.JWTTokens[j] = token
-				needNormalize = true
-			}
-		}
-		proj.Spec.Roles[i] = role
-	}
-	for _, roleTokenEntry := range proj.Status.JWTTokensByRole {
-		for j, token := range roleTokenEntry.Items {
-			if token.ID == "" {
-				token.ID = strconv.FormatInt(token.IssuedAt, 10)
-				roleTokenEntry.Items[j] = token
-				needNormalize = true
-			}
-		}
-	}
-	needSync := syncJWTTokenBetweenStatusAndSpec(proj)
-	return needNormalize || needSync
-}
-
-func syncJWTTokenBetweenStatusAndSpec(proj *AppProject) bool {
-	existingRole := map[string]bool{}
-	needSync := false
-	for roleIndex, role := range proj.Spec.Roles {
-		existingRole[role.Name] = true
-
-		tokensInSpec := role.JWTTokens
-		tokensInStatus := []JWTToken{}
-		if proj.Status.JWTTokensByRole == nil {
-			tokensByRole := make(map[string]JWTTokens)
-			proj.Status.JWTTokensByRole = tokensByRole
-		} else {
-			tokensInStatus = proj.Status.JWTTokensByRole[role.Name].Items
-		}
-		tokens := jwtTokensCombine(tokensInStatus, tokensInSpec)
-
-		sort.Slice(proj.Spec.Roles[roleIndex].JWTTokens, func(i, j int) bool {
-			return proj.Spec.Roles[roleIndex].JWTTokens[i].IssuedAt > proj.Spec.Roles[roleIndex].JWTTokens[j].IssuedAt
-		})
-		sort.Slice(proj.Status.JWTTokensByRole[role.Name].Items, func(i, j int) bool {
-			return proj.Status.JWTTokensByRole[role.Name].Items[i].IssuedAt > proj.Status.JWTTokensByRole[role.Name].Items[j].IssuedAt
-		})
-		if !cmp.Equal(tokens, proj.Spec.Roles[roleIndex].JWTTokens) || !cmp.Equal(tokens, proj.Status.JWTTokensByRole[role.Name].Items) {
-			needSync = true
-		}
-
-		proj.Spec.Roles[roleIndex].JWTTokens = tokens
-		proj.Status.JWTTokensByRole[role.Name] = JWTTokens{Items: tokens}
-	}
-	if proj.Status.JWTTokensByRole != nil {
-		for role := range proj.Status.JWTTokensByRole {
-			if !existingRole[role] {
-				delete(proj.Status.JWTTokensByRole, role)
-				needSync = true
-			}
-		}
-	}
-
-	return needSync
-}
-
-func jwtTokensCombine(tokens1 []JWTToken, tokens2 []JWTToken) []JWTToken {
-	tokensMap := make(map[string]JWTToken)
-	for _, token := range append(tokens1, tokens2...) {
-		tokensMap[token.ID] = token
-	}
-
-	var tokens []JWTToken
-	for _, v := range tokensMap {
-		tokens = append(tokens, v)
-	}
-
-	sort.Slice(tokens, func(i, j int) bool {
-		return tokens[i].IssuedAt > tokens[j].IssuedAt
-	})
-	return tokens
-}
diff --git a/reposerver/apiclient/repository.pb.go b/reposerver/apiclient/repository.pb.go
index 3d8f45749..017a0e8a6 100644
--- a/reposerver/apiclient/repository.pb.go
+++ b/reposerver/apiclient/repository.pb.go
@@ -49,6 +49,7 @@ type ManifestRequest struct {
 	// Request to verify the signature when generating the manifests (only for Git repositories)
 	VerifySignature      bool                  `protobuf:"varint,16,opt,name=verifySignature,proto3" json:"verifySignature,omitempty"`
 	HelmRepoCreds        []*v1alpha1.RepoCreds `protobuf:"bytes,17,rep,name=helmRepoCreds,proto3" json:"helmRepoCreds,omitempty"`
+	NoRevisionCache      bool                  `protobuf:"varint,18,opt,name=noRevisionCache,proto3" json:"noRevisionCache,omitempty"`
 	XXX_NoUnkeyedLiteral struct{}              `json:"-"`
 	XXX_unrecognized     []byte                `json:"-"`
 	XXX_sizecache        int32                 `json:"-"`
@@ -185,6 +186,13 @@ func (m *ManifestRequest) GetHelmRepoCreds() []*v1alpha1.RepoCreds {
 	return nil
 }
 
+func (m *ManifestRequest) GetNoRevisionCache() bool {
+	if m != nil {
+		return m.NoRevisionCache
+	}
+	return false
+}
+
 // TestRepositoryRequest is a query to test repository is valid or not and has valid access.
 type TestRepositoryRequest struct {
 	Repo                 *v1alpha1.Repository `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"`
@@ -1343,93 +1351,94 @@ func init() {
 }
 
 var fileDescriptor_dd8723cfcc820480 = []byte{
-	// 1371 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x18, 0xcb, 0x6e, 0xdb, 0xc6,
-	0xd6, 0x94, 0xe4, 0x87, 0x8e, 0x12, 0x5b, 0x9e, 0x3c, 0x2e, 0xaf, 0xae, 0x23, 0x38, 0x04, 0x6e,
-	0xe0, 0x36, 0x0d, 0x85, 0x28, 0x41, 0x1b, 0x24, 0x40, 0x01, 0xd5, 0x49, 0x1c, 0xc0, 0x49, 0xec,
-	0xd2, 0x69, 0x81, 0x16, 0x41, 0x83, 0x31, 0x75, 0x4c, 0x4d, 0x25, 0x91, 0x13, 0x92, 0x52, 0xe1,
-	0x00, 0x5d, 0x16, 0x5d, 0x74, 0xdd, 0xfe, 0x42, 0x3f, 0xa3, 0xab, 0x3e, 0x96, 0x5d, 0x76, 0x59,
-	0xe4, 0x4b, 0x8a, 0x19, 0xbe, 0x86, 0x14, 0xed, 0x2e, 0x94, 0x38, 0x1b, 0x7b, 0xe6, 0xbc, 0xe7,
-	0xbc, 0x45, 0xb8, 0xe6, 0x23, 0xf7, 0x02, 0xf4, 0xa7, 0xe8, 0x77, 0xe4, 0x91, 0x85, 0x9e, 0x7f,
-	0xac, 0x1c, 0x4d, 0xee, 0x7b, 0xa1, 0x47, 0x20, 0x83, 0xb4, 0x2e, 0x3a, 0x9e, 0xe3, 0x49, 0x70,
-	0x47, 0x9c, 0x22, 0x8a, 0xd6, 0x86, 0xe3, 0x79, 0xce, 0x08, 0x3b, 0x94, 0xb3, 0x0e, 0x75, 0x5d,
-	0x2f, 0xa4, 0x21, 0xf3, 0xdc, 0x20, 0xc6, 0x1a, 0xc3, 0x3b, 0x81, 0xc9, 0x3c, 0x89, 0xb5, 0x3d,
-	0x1f, 0x3b, 0xd3, 0x9b, 0x1d, 0x07, 0x5d, 0xf4, 0x69, 0x88, 0xfd, 0x98, 0xe6, 0xb1, 0xc3, 0xc2,
-	0xc1, 0xe4, 0xd0, 0xb4, 0xbd, 0x71, 0x87, 0xfa, 0x52, 0xc5, 0xd7, 0xf2, 0x70, 0xc3, 0xee, 0x77,
-	0xa6, 0xdd, 0x0e, 0x1f, 0x3a, 0x82, 0x3f, 0xe8, 0x50, 0xce, 0x47, 0xcc, 0x96, 0xf2, 0x3b, 0xd3,
-	0x9b, 0x74, 0xc4, 0x07, 0x74, 0x46, 0x9a, 0xf1, 0xf3, 0x32, 0xac, 0x3d, 0xa1, 0x2e, 0x3b, 0xc2,
-	0x20, 0xb4, 0xf0, 0xe5, 0x04, 0x83, 0x90, 0x3c, 0x87, 0x9a, 0x78, 0x87, 0xae, 0x6d, 0x6a, 0x5b,
-	0x8d, 0xee, 0x23, 0x33, 0x53, 0x68, 0x26, 0x0a, 0xe5, 0xe1, 0x85, 0xdd, 0x37, 0xa7, 0x5d, 0x93,
-	0x0f, 0x1d, 0x53, 0x28, 0x34, 0x15, 0x85, 0x66, 0xa2, 0xd0, 0xb4, 0x52, 0x8f, 0x58, 0x52, 0x2a,
-	0x69, 0xc1, 0x8a, 0x8f, 0x53, 0x16, 0x30, 0xcf, 0xd5, 0x2b, 0x9b, 0xda, 0x56, 0xdd, 0x4a, 0xef,
-	0x44, 0x87, 0x65, 0xd7, 0xdb, 0xa6, 0xf6, 0x00, 0xf5, 0xea, 0xa6, 0xb6, 0xb5, 0x62, 0x25, 0x57,
-	0xb2, 0x09, 0x0d, 0xca, 0xf9, 0x63, 0x7a, 0x88, 0xa3, 0x5d, 0x3c, 0xd6, 0x6b, 0x92, 0x51, 0x05,
-	0x09, 0x5e, 0xca, 0xf9, 0x53, 0x3a, 0x46, 0x7d, 0x51, 0x62, 0x93, 0x2b, 0xd9, 0x80, 0xba, 0x4b,
-	0xc7, 0x18, 0x70, 0x6a, 0xa3, 0xbe, 0x22, 0x71, 0x19, 0x80, 0x7c, 0x0b, 0xeb, 0x8a, 0xe1, 0x07,
-	0xde, 0xc4, 0xb7, 0x51, 0x07, 0xf9, 0xf4, 0xbd, 0xf9, 0x9e, 0xde, 0x2b, 0x8a, 0xb5, 0x66, 0x35,
-	0x91, 0xaf, 0x60, 0x51, 0x26, 0x8d, 0xde, 0xd8, 0xac, 0xbe, 0x51, 0x6f, 0x47, 0x62, 0x89, 0x0b,
-	0xcb, 0x7c, 0x34, 0x71, 0x98, 0x1b, 0xe8, 0xe7, 0xa4, 0x86, 0x67, 0xf3, 0x69, 0xd8, 0xf6, 0xdc,
-	0x23, 0xe6, 0x3c, 0xa1, 0x2e, 0x75, 0x70, 0x8c, 0x6e, 0xb8, 0x2f, 0x85, 0x5b, 0x89, 0x12, 0xf2,
-	0x0a, 0x9a, 0xc3, 0x49, 0x10, 0x7a, 0x63, 0xf6, 0x0a, 0xf7, 0xb8, 0x4c, 0x6e, 0xfd, 0xbc, 0xf4,
-	0xe6, 0xd3, 0xf9, 0x14, 0xef, 0x16, 0xa4, 0x5a, 0x33, 0x7a, 0x44, 0x92, 0x0c, 0x27, 0x87, 0xf8,
-	0x39, 0xfa, 0x32, 0xbb, 0x56, 0xa3, 0x24, 0x51, 0x40, 0x51, 0x1a, 0xb1, 0xf8, 0x16, 0xe8, 0x6b,
-	0x9b, 0xd5, 0x28, 0x8d, 0x52, 0x10, 0xd9, 0x82, 0xb5, 0x29, 0xfa, 0xec, 0xe8, 0xf8, 0x80, 0x39,
-	0x2e, 0x0d, 0x27, 0x3e, 0xea, 0x4d, 0x99, 0x8a, 0x45, 0x30, 0x19, 0xc3, 0xf9, 0x01, 0x8e, 0xc6,
-	0xc2, 0xe5, 0xdb, 0x3e, 0xf6, 0x03, 0x7d, 0x5d, 0xfa, 0x77, 0x67, 0xfe, 0x08, 0x4a, 0x71, 0x56,
-	0x5e, 0xba, 0x31, 0x81, 0x4b, 0xcf, 0x64, 0x91, 0xa6, 0x11, 0x3e, 0x8b, 0x72, 0x35, 0x1e, 0xc1,
-	0xe5, 0xa2, 0xda, 0x80, 0x7b, 0x6e, 0x80, 0xc4, 0x04, 0x22, 0x5d, 0xc2, 0xb0, 0x9f, 0x61, 0xa5,
-	0x15, 0x2b, 0x56, 0x09, 0xc6, 0xf8, 0x4d, 0x83, 0x66, 0xd6, 0x6a, 0x62, 0x21, 0x1b, 0x50, 0x1f,
-	0xc7, 0xb0, 0x40, 0xd7, 0x64, 0x38, 0x32, 0x40, 0xbe, 0x72, 0x2b, 0xc5, 0xca, 0xbd, 0x0c, 0x4b,
-	0x51, 0x4f, 0x96, 0xcd, 0xa2, 0x6e, 0xc5, 0xb7, 0x5c, 0x87, 0xa9, 0x15, 0x3a, 0x4c, 0x1b, 0x20,
-	0x90, 0x85, 0xf7, 0xec, 0x98, 0xa3, 0xbe, 0x24, 0xb1, 0x0a, 0x84, 0x18, 0x70, 0x2e, 0x8a, 0xb3,
-	0x85, 0xc1, 0x64, 0x14, 0xea, 0xcb, 0x92, 0x22, 0x07, 0x33, 0x3c, 0x58, 0x7b, 0xcc, 0xc4, 0x1b,
-	0x8e, 0x82, 0xb3, 0x89, 0xc1, 0x87, 0x50, 0x13, 0xca, 0xc4, 0xc3, 0x0e, 0x7d, 0xea, 0xda, 0x03,
-	0x4c, 0x7c, 0x95, 0xde, 0x09, 0x81, 0x5a, 0x48, 0x9d, 0x40, 0xaf, 0x48, 0xb8, 0x3c, 0x1b, 0x3f,
-	0x68, 0x91, 0xa5, 0x3d, 0xce, 0x83, 0x77, 0xde, 0xdc, 0x8d, 0x09, 0x2c, 0xf7, 0x38, 0x17, 0xf6,
-	0x90, 0x9b, 0x50, 0xa3, 0x9c, 0x47, 0x8f, 0x68, 0x74, 0xaf, 0x98, 0xca, 0x20, 0x8d, 0x49, 0xc4,
-	0xff, 0xe0, 0x81, 0x1b, 0x0a, 0xc9, 0x82, 0xb4, 0xf5, 0x11, 0xd4, 0x53, 0x10, 0x69, 0x42, 0x75,
-	0x88, 0x51, 0xae, 0xd5, 0x2d, 0x71, 0x24, 0x17, 0x61, 0x71, 0x4a, 0x47, 0x93, 0x24, 0x4b, 0xa2,
-	0xcb, 0xdd, 0xca, 0x1d, 0xcd, 0xf8, 0xab, 0x0a, 0xff, 0x15, 0x76, 0x1e, 0xc8, 0xe4, 0xe8, 0x71,
-	0x7e, 0x1f, 0x43, 0xca, 0x46, 0xc1, 0xa7, 0x13, 0xf4, 0x8f, 0xdf, 0xb2, 0x3b, 0x1c, 0x58, 0x8a,
-	0x72, 0x4b, 0x9a, 0xf5, 0x16, 0x06, 0x4a, 0x2c, 0x3e, 0x9b, 0x22, 0xd5, 0xb7, 0x33, 0x45, 0xca,
-	0xba, 0x7a, 0xed, 0x8c, 0xba, 0xfa, 0x89, 0x83, 0xdd, 0xf8, 0xbe, 0x02, 0x97, 0x85, 0xad, 0x59,
-	0x50, 0xd3, 0xbe, 0x22, 0xca, 0x41, 0x54, 0x78, 0x94, 0x22, 0xf2, 0x4c, 0x6e, 0xc3, 0xf2, 0x30,
-	0xf0, 0x5c, 0x17, 0xc3, 0x38, 0x1c, 0x2d, 0x35, 0xf1, 0x76, 0x23, 0x54, 0x8f, 0xf3, 0x03, 0x8e,
-	0xb6, 0x95, 0x90, 0x92, 0xeb, 0x50, 0x13, 0x8d, 0x58, 0xf6, 0x98, 0x46, 0xf7, 0x3f, 0x2a, 0xcb,
-	0x23, 0x1c, 0x8d, 0x13, 0x7a, 0x49, 0x44, 0xee, 0x42, 0x3d, 0xb5, 0x3f, 0x76, 0xd0, 0x46, 0x4e,
-	0x49, 0x82, 0x4c, 0xd8, 0x32, 0x72, 0xc1, 0xdb, 0x67, 0x3e, 0xda, 0xb2, 0x8d, 0x2e, 0xce, 0xf2,
-	0xde, 0x4f, 0x90, 0x29, 0x6f, 0x4a, 0x6e, 0xfc, 0xaa, 0xc1, 0xd5, 0x2c, 0xc9, 0xad, 0xb8, 0xe4,
-	0x9e, 0x60, 0x48, 0xfb, 0x34, 0xa4, 0xef, 0x7e, 0xb1, 0xbb, 0x06, 0xab, 0xf6, 0x00, 0xed, 0x61,
-	0x36, 0x54, 0xa3, 0xfd, 0xae, 0x00, 0x35, 0x7e, 0xaf, 0xc0, 0x6a, 0x3e, 0x10, 0x22, 0x92, 0xa2,
-	0xe5, 0x27, 0x91, 0x14, 0x67, 0xb2, 0x0f, 0xe7, 0xd0, 0x9d, 0x32, 0xdf, 0x73, 0xc5, 0x0a, 0x92,
-	0x64, 0xfd, 0x07, 0x27, 0x87, 0xd3, 0x7c, 0xa0, 0x90, 0x47, 0x6d, 0x25, 0x27, 0x81, 0xb8, 0x00,
-	0x9c, 0xfa, 0x74, 0x8c, 0x21, 0xfa, 0x22, 0xb5, 0xab, 0x6f, 0x20, 0xb5, 0x23, 0x0b, 0xf6, 0x13,
-	0xb1, 0x96, 0xa2, 0xa1, 0xf5, 0x02, 0xd6, 0x67, 0x4c, 0x2a, 0x69, 0x6b, 0xb7, 0xd5, 0xb6, 0xd6,
-	0xe8, 0xb6, 0x4b, 0x5e, 0xa8, 0x88, 0x51, 0xdb, 0xde, 0x2f, 0x15, 0x68, 0x28, 0xf9, 0x59, 0xea,
-	0xc6, 0x36, 0x80, 0x64, 0x78, 0xc8, 0x46, 0x18, 0x39, 0xb1, 0x6e, 0x29, 0x10, 0x32, 0x2c, 0x71,
-	0xca, 0xee, 0x7c, 0x4e, 0x11, 0x26, 0x95, 0x7a, 0x44, 0x4c, 0x73, 0xa9, 0x3a, 0x88, 0xab, 0x3c,
-	0xbe, 0x91, 0x6f, 0x60, 0xf5, 0x88, 0x8d, 0x70, 0x3f, 0x33, 0x64, 0x49, 0x1a, 0xb2, 0x37, 0xbf,
-	0x21, 0x0f, 0x55, 0xb9, 0x56, 0x41, 0x8d, 0xf1, 0x3e, 0x34, 0x8b, 0xe5, 0x2a, 0x8c, 0x64, 0x63,
-	0xea, 0xa4, 0xde, 0x8a, 0x6f, 0xc6, 0x8f, 0x1a, 0x90, 0xd9, 0x78, 0x9c, 0xe4, 0xf4, 0xe1, 0x9d,
-	0x20, 0xd9, 0x51, 0xa3, 0x42, 0x51, 0x20, 0x64, 0x17, 0x1a, 0x7d, 0x0c, 0x42, 0xe6, 0x4a, 0x83,
-	0xe3, 0x26, 0xf2, 0xde, 0xe9, 0x81, 0xbf, 0x9f, 0x31, 0x58, 0x2a, 0xb7, 0xf1, 0x19, 0x5c, 0x39,
-	0x95, 0x5a, 0xd9, 0xa1, 0xb4, 0xdc, 0x0e, 0x75, 0xea, 0xe6, 0x65, 0x10, 0x68, 0x16, 0xbb, 0x91,
-	0xf1, 0x12, 0xd6, 0x85, 0x4f, 0xb7, 0x07, 0xd4, 0x0f, 0xcf, 0x68, 0x2f, 0xba, 0x07, 0xf5, 0x54,
-	0x65, 0xa9, 0xaf, 0x5b, 0xb0, 0x32, 0x4d, 0x76, 0xfd, 0x68, 0x31, 0x4a, 0xef, 0x46, 0x0f, 0x88,
-	0x6a, 0x6f, 0x3c, 0x37, 0xae, 0xc3, 0x22, 0x0b, 0x71, 0x9c, 0xac, 0x26, 0x97, 0x8a, 0xed, 0x5e,
-	0x92, 0x5b, 0x11, 0x4d, 0xf7, 0xbb, 0x45, 0x58, 0xcf, 0xba, 0xae, 0xf8, 0xcb, 0x6c, 0x24, 0x7b,
-	0xd0, 0xdc, 0x89, 0x7f, 0x65, 0x27, 0xeb, 0x2e, 0xf9, 0x9f, 0x2a, 0xa7, 0xf0, 0x7b, 0xbb, 0xb5,
-	0x51, 0x8e, 0x8c, 0x2c, 0x32, 0x16, 0xc8, 0x17, 0xb0, 0x9a, 0x5f, 0xc1, 0xc9, 0x55, 0x95, 0xa3,
-	0xf4, 0x57, 0x41, 0xcb, 0x38, 0x8d, 0x24, 0x15, 0x7d, 0x0f, 0x56, 0x92, 0x55, 0x36, 0x6f, 0x63,
-	0x61, 0xc1, 0x6d, 0x35, 0x55, 0xa4, 0x40, 0x18, 0x0b, 0xe4, 0xe3, 0x88, 0x59, 0xac, 0x65, 0xb3,
-	0xcc, 0xca, 0xce, 0xd9, 0xba, 0x50, 0xb2, 0xe0, 0x19, 0x0b, 0xe4, 0x39, 0x9c, 0xdf, 0x91, 0x1d,
-	0x3a, 0x1e, 0xde, 0xe4, 0xff, 0x79, 0x25, 0x27, 0xec, 0x6c, 0xf9, 0xa7, 0x95, 0xcf, 0x7f, 0x63,
-	0x81, 0xfc, 0xa4, 0xc1, 0x85, 0x1d, 0x0c, 0x8b, 0xb3, 0x90, 0xdc, 0x28, 0x57, 0x72, 0xc2, 0xcc,
-	0x6c, 0x3d, 0x9d, 0x37, 0x67, 0xf3, 0x62, 0x8d, 0x05, 0xb2, 0x2f, 0x9f, 0x9d, 0xe5, 0x1e, 0xb9,
-	0x52, 0x9a, 0x64, 0xa9, 0xf7, 0xda, 0x27, 0xa1, 0x93, 0xa7, 0x7e, 0xd2, 0xfb, 0xe3, 0x75, 0x5b,
-	0xfb, 0xf3, 0x75, 0x5b, 0xfb, 0xfb, 0x75, 0x5b, 0xfb, 0xf2, 0xd6, 0xbf, 0x7c, 0x20, 0x52, 0xbe,
-	0x65, 0x51, 0xce, 0xec, 0x11, 0x43, 0x37, 0x3c, 0x5c, 0x92, 0x9f, 0x83, 0x6e, 0xfd, 0x13, 0x00,
-	0x00, 0xff, 0xff, 0x5d, 0x88, 0x0d, 0x91, 0xea, 0x12, 0x00, 0x00,
+	// 1386 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x58, 0xcb, 0x6f, 0xdb, 0x46,
+	0x13, 0x37, 0x25, 0xf9, 0xa1, 0x51, 0x62, 0xcb, 0x9b, 0xc7, 0xc7, 0x4f, 0x9f, 0x23, 0x38, 0x04,
+	0xbe, 0xc0, 0x6d, 0x1a, 0x0a, 0x51, 0x82, 0x36, 0x48, 0x80, 0x02, 0xaa, 0x93, 0x38, 0x80, 0x93,
+	0xd8, 0xa5, 0xd3, 0x02, 0x2d, 0x82, 0x06, 0x6b, 0x6a, 0x4c, 0x6d, 0x25, 0x91, 0x1b, 0x92, 0x52,
+	0xe1, 0x00, 0x3d, 0x16, 0x3d, 0xf4, 0xdc, 0xfe, 0x3b, 0x3d, 0xf5, 0x71, 0x6c, 0x6f, 0x3d, 0x16,
+	0xf9, 0x4b, 0x8a, 0x5d, 0xbe, 0x96, 0x14, 0xed, 0x1e, 0x94, 0x38, 0x17, 0x7b, 0x77, 0xde, 0x3b,
+	0xfb, 0xdb, 0x99, 0xa1, 0xe0, 0x9a, 0x8f, 0xdc, 0x0b, 0xd0, 0x9f, 0xa2, 0xdf, 0x91, 0x4b, 0x16,
+	0x7a, 0xfe, 0xb1, 0xb2, 0x34, 0xb9, 0xef, 0x85, 0x1e, 0x81, 0x8c, 0xd2, 0xba, 0xe8, 0x78, 0x8e,
+	0x27, 0xc9, 0x1d, 0xb1, 0x8a, 0x24, 0x5a, 0x1b, 0x8e, 0xe7, 0x39, 0x23, 0xec, 0x50, 0xce, 0x3a,
+	0xd4, 0x75, 0xbd, 0x90, 0x86, 0xcc, 0x73, 0x83, 0x98, 0x6b, 0x0c, 0xef, 0x04, 0x26, 0xf3, 0x24,
+	0xd7, 0xf6, 0x7c, 0xec, 0x4c, 0x6f, 0x76, 0x1c, 0x74, 0xd1, 0xa7, 0x21, 0xf6, 0x63, 0x99, 0xc7,
+	0x0e, 0x0b, 0x07, 0x93, 0x43, 0xd3, 0xf6, 0xc6, 0x1d, 0xea, 0x4b, 0x17, 0x5f, 0xcb, 0xc5, 0x0d,
+	0xbb, 0xdf, 0x99, 0x76, 0x3b, 0x7c, 0xe8, 0x08, 0xfd, 0xa0, 0x43, 0x39, 0x1f, 0x31, 0x5b, 0xda,
+	0xef, 0x4c, 0x6f, 0xd2, 0x11, 0x1f, 0xd0, 0x19, 0x6b, 0xc6, 0x9f, 0xcb, 0xb0, 0xf6, 0x84, 0xba,
+	0xec, 0x08, 0x83, 0xd0, 0xc2, 0x97, 0x13, 0x0c, 0x42, 0xf2, 0x1c, 0x6a, 0xe2, 0x1c, 0xba, 0xb6,
+	0xa9, 0x6d, 0x35, 0xba, 0x8f, 0xcc, 0xcc, 0xa1, 0x99, 0x38, 0x94, 0x8b, 0x17, 0x76, 0xdf, 0x9c,
+	0x76, 0x4d, 0x3e, 0x74, 0x4c, 0xe1, 0xd0, 0x54, 0x1c, 0x9a, 0x89, 0x43, 0xd3, 0x4a, 0x33, 0x62,
+	0x49, 0xab, 0xa4, 0x05, 0x2b, 0x3e, 0x4e, 0x59, 0xc0, 0x3c, 0x57, 0xaf, 0x6c, 0x6a, 0x5b, 0x75,
+	0x2b, 0xdd, 0x13, 0x1d, 0x96, 0x5d, 0x6f, 0x9b, 0xda, 0x03, 0xd4, 0xab, 0x9b, 0xda, 0xd6, 0x8a,
+	0x95, 0x6c, 0xc9, 0x26, 0x34, 0x28, 0xe7, 0x8f, 0xe9, 0x21, 0x8e, 0x76, 0xf1, 0x58, 0xaf, 0x49,
+	0x45, 0x95, 0x24, 0x74, 0x29, 0xe7, 0x4f, 0xe9, 0x18, 0xf5, 0x45, 0xc9, 0x4d, 0xb6, 0x64, 0x03,
+	0xea, 0x2e, 0x1d, 0x63, 0xc0, 0xa9, 0x8d, 0xfa, 0x8a, 0xe4, 0x65, 0x04, 0xf2, 0x2d, 0xac, 0x2b,
+	0x81, 0x1f, 0x78, 0x13, 0xdf, 0x46, 0x1d, 0xe4, 0xd1, 0xf7, 0xe6, 0x3b, 0x7a, 0xaf, 0x68, 0xd6,
+	0x9a, 0xf5, 0x44, 0xbe, 0x82, 0x45, 0x09, 0x1a, 0xbd, 0xb1, 0x59, 0x7d, 0xa3, 0xd9, 0x8e, 0xcc,
+	0x12, 0x17, 0x96, 0xf9, 0x68, 0xe2, 0x30, 0x37, 0xd0, 0xcf, 0x49, 0x0f, 0xcf, 0xe6, 0xf3, 0xb0,
+	0xed, 0xb9, 0x47, 0xcc, 0x79, 0x42, 0x5d, 0xea, 0xe0, 0x18, 0xdd, 0x70, 0x5f, 0x1a, 0xb7, 0x12,
+	0x27, 0xe4, 0x15, 0x34, 0x87, 0x93, 0x20, 0xf4, 0xc6, 0xec, 0x15, 0xee, 0x71, 0x09, 0x6e, 0xfd,
+	0xbc, 0xcc, 0xe6, 0xd3, 0xf9, 0x1c, 0xef, 0x16, 0xac, 0x5a, 0x33, 0x7e, 0x04, 0x48, 0x86, 0x93,
+	0x43, 0xfc, 0x1c, 0x7d, 0x89, 0xae, 0xd5, 0x08, 0x24, 0x0a, 0x29, 0x82, 0x11, 0x8b, 0x77, 0x81,
+	0xbe, 0xb6, 0x59, 0x8d, 0x60, 0x94, 0x92, 0xc8, 0x16, 0xac, 0x4d, 0xd1, 0x67, 0x47, 0xc7, 0x07,
+	0xcc, 0x71, 0x69, 0x38, 0xf1, 0x51, 0x6f, 0x4a, 0x28, 0x16, 0xc9, 0x64, 0x0c, 0xe7, 0x07, 0x38,
+	0x1a, 0x8b, 0x94, 0x6f, 0xfb, 0xd8, 0x0f, 0xf4, 0x75, 0x99, 0xdf, 0x9d, 0xf9, 0x6f, 0x50, 0x9a,
+	0xb3, 0xf2, 0xd6, 0x45, 0x60, 0xae, 0x67, 0xc5, 0x2f, 0x25, 0x7a, 0x23, 0x24, 0x0a, 0xac, 0x40,
+	0x36, 0x26, 0x70, 0xe9, 0x99, 0x7c, 0xce, 0x29, 0x16, 0xce, 0xe2, 0x61, 0x1b, 0x8f, 0xe0, 0x72,
+	0xd1, 0x6d, 0xc0, 0x3d, 0x37, 0x40, 0x62, 0x02, 0x91, 0xc9, 0x63, 0xd8, 0xcf, 0xb8, 0x32, 0x8a,
+	0x15, 0xab, 0x84, 0x63, 0xfc, 0xaa, 0x41, 0x33, 0x2b, 0x4a, 0xb1, 0x91, 0x0d, 0xa8, 0x8f, 0x63,
+	0x5a, 0xa0, 0x6b, 0xf2, 0xe2, 0x32, 0x42, 0xfe, 0x8d, 0x57, 0x8a, 0x6f, 0xfc, 0x32, 0x2c, 0x45,
+	0xd5, 0x5b, 0x96, 0x95, 0xba, 0x15, 0xef, 0x72, 0xb5, 0xa8, 0x56, 0xa8, 0x45, 0x6d, 0x80, 0x40,
+	0x3e, 0xd1, 0x67, 0xc7, 0x1c, 0xf5, 0x25, 0xc9, 0x55, 0x28, 0xc4, 0x80, 0x73, 0x11, 0x22, 0x2c,
+	0x0c, 0x26, 0xa3, 0x50, 0x5f, 0x96, 0x12, 0x39, 0x9a, 0xe1, 0xc1, 0xda, 0x63, 0x26, 0xce, 0x70,
+	0x14, 0x9c, 0xcd, 0x1d, 0x7c, 0x08, 0x35, 0xe1, 0x4c, 0x1c, 0xec, 0xd0, 0xa7, 0xae, 0x3d, 0xc0,
+	0x24, 0x57, 0xe9, 0x9e, 0x10, 0xa8, 0x85, 0xd4, 0x09, 0xf4, 0x8a, 0xa4, 0xcb, 0xb5, 0xf1, 0x83,
+	0x16, 0x45, 0xda, 0xe3, 0x3c, 0x78, 0xe7, 0x6d, 0xc0, 0x98, 0xc0, 0x72, 0x8f, 0x73, 0x11, 0x0f,
+	0xb9, 0x09, 0x35, 0xca, 0x79, 0x74, 0x88, 0x46, 0xf7, 0x8a, 0xa9, 0xb4, 0xdc, 0x58, 0x44, 0xfc,
+	0x0f, 0x1e, 0xb8, 0xa1, 0xb0, 0x2c, 0x44, 0x5b, 0x1f, 0x41, 0x3d, 0x25, 0x91, 0x26, 0x54, 0x87,
+	0x18, 0x61, 0xad, 0x6e, 0x89, 0x25, 0xb9, 0x08, 0x8b, 0x53, 0x3a, 0x9a, 0x24, 0x28, 0x89, 0x36,
+	0x77, 0x2b, 0x77, 0x34, 0xe3, 0xaf, 0x2a, 0xfc, 0x57, 0xc4, 0x79, 0x20, 0xc1, 0xd1, 0xe3, 0xfc,
+	0x3e, 0x86, 0x94, 0x8d, 0x82, 0x4f, 0x27, 0xe8, 0x1f, 0xbf, 0xe5, 0x74, 0x38, 0xb0, 0x14, 0x61,
+	0x4b, 0x86, 0xf5, 0x16, 0x5a, 0x4f, 0x6c, 0x3e, 0xeb, 0x37, 0xd5, 0xb7, 0xd3, 0x6f, 0xca, 0xea,
+	0x7f, 0xed, 0x8c, 0xea, 0xff, 0x89, 0x23, 0x80, 0xf1, 0x7d, 0x05, 0x2e, 0x8b, 0x58, 0xb3, 0x4b,
+	0x4d, 0xeb, 0x8a, 0x78, 0x0e, 0xe2, 0x85, 0x47, 0x10, 0x91, 0x6b, 0x72, 0x1b, 0x96, 0x87, 0x81,
+	0xe7, 0xba, 0x18, 0xc6, 0xd7, 0xd1, 0x52, 0x81, 0xb7, 0x1b, 0xb1, 0x7a, 0x9c, 0x1f, 0x70, 0xb4,
+	0xad, 0x44, 0x94, 0x5c, 0x87, 0x9a, 0x28, 0xd9, 0xb2, 0xc6, 0x34, 0xba, 0xff, 0x51, 0x55, 0x1e,
+	0xe1, 0x68, 0x9c, 0xc8, 0x4b, 0x21, 0x72, 0x17, 0xea, 0x69, 0xfc, 0x71, 0x82, 0x36, 0x72, 0x4e,
+	0x12, 0x66, 0xa2, 0x96, 0x89, 0x0b, 0xdd, 0x3e, 0xf3, 0xd1, 0x96, 0x65, 0x74, 0x71, 0x56, 0xf7,
+	0x7e, 0xc2, 0x4c, 0x75, 0x53, 0x71, 0xe3, 0x17, 0x0d, 0xae, 0x66, 0x20, 0x4f, 0x1a, 0xc7, 0x13,
+	0x0c, 0x69, 0x9f, 0x86, 0xf4, 0xdd, 0x8f, 0x80, 0xd7, 0x60, 0xd5, 0x1e, 0xa0, 0x3d, 0xcc, 0xda,
+	0x6f, 0x34, 0x09, 0x16, 0xa8, 0xc6, 0x6f, 0x15, 0x58, 0xcd, 0x5f, 0x84, 0xb8, 0x49, 0x51, 0xf2,
+	0x93, 0x9b, 0x14, 0x6b, 0xb2, 0x0f, 0xe7, 0xd0, 0x9d, 0x32, 0xdf, 0x73, 0xc5, 0xb0, 0x92, 0xa0,
+	0xfe, 0x83, 0x93, 0xaf, 0xd3, 0x7c, 0xa0, 0x88, 0x47, 0x65, 0x25, 0x67, 0x81, 0xb8, 0x00, 0x9c,
+	0xfa, 0x74, 0x8c, 0x21, 0xfa, 0x02, 0xda, 0xd5, 0x37, 0x00, 0xed, 0x28, 0x82, 0xfd, 0xc4, 0xac,
+	0xa5, 0x78, 0x68, 0xbd, 0x80, 0xf5, 0x99, 0x90, 0x4a, 0xca, 0xda, 0x6d, 0xb5, 0xac, 0x35, 0xba,
+	0xed, 0x92, 0x13, 0x2a, 0x66, 0xd4, 0xb2, 0xf7, 0x73, 0x05, 0x1a, 0x0a, 0x3e, 0x4b, 0xd3, 0xd8,
+	0x06, 0x90, 0x0a, 0x0f, 0xd9, 0x08, 0xa3, 0x24, 0xd6, 0x2d, 0x85, 0x42, 0x86, 0x25, 0x49, 0xd9,
+	0x9d, 0x2f, 0x29, 0x22, 0xa4, 0xd2, 0x8c, 0x88, 0x6e, 0x2e, 0x5d, 0x07, 0xf1, 0x2b, 0x8f, 0x77,
+	0xe4, 0x1b, 0x58, 0x3d, 0x62, 0x23, 0xdc, 0xcf, 0x02, 0x59, 0x92, 0x81, 0xec, 0xcd, 0x1f, 0xc8,
+	0x43, 0xd5, 0xae, 0x55, 0x70, 0x63, 0xbc, 0x0f, 0xcd, 0xe2, 0x73, 0x15, 0x41, 0xb2, 0x31, 0x75,
+	0xd2, 0x6c, 0xc5, 0x3b, 0xe3, 0x47, 0x0d, 0xc8, 0xec, 0x7d, 0x9c, 0x94, 0xf4, 0xe1, 0x9d, 0x20,
+	0x99, 0x66, 0xa3, 0x87, 0xa2, 0x50, 0xc8, 0x2e, 0x34, 0xfa, 0x18, 0x84, 0xcc, 0x95, 0x01, 0xc7,
+	0x45, 0xe4, 0xbd, 0xd3, 0x2f, 0xfe, 0x7e, 0xa6, 0x60, 0xa9, 0xda, 0xc6, 0x67, 0x70, 0xe5, 0x54,
+	0x69, 0x65, 0x86, 0xd2, 0x72, 0x33, 0xd4, 0xa9, 0x93, 0x97, 0x41, 0xa0, 0x59, 0xac, 0x46, 0xc6,
+	0x4b, 0x58, 0x17, 0x39, 0xdd, 0x1e, 0x50, 0x3f, 0x3c, 0xa3, 0xb9, 0xe8, 0x1e, 0xd4, 0x53, 0x97,
+	0xa5, 0xb9, 0x6e, 0xc1, 0xca, 0x34, 0xf9, 0x2a, 0x88, 0x06, 0xa3, 0x74, 0x6f, 0xf4, 0x80, 0xa8,
+	0xf1, 0xc6, 0x7d, 0xe3, 0x3a, 0x2c, 0xb2, 0x10, 0xc7, 0xc9, 0x68, 0x72, 0xa9, 0x58, 0xee, 0xa5,
+	0xb8, 0x15, 0xc9, 0x74, 0xbf, 0x5b, 0x84, 0xf5, 0xac, 0xea, 0x8a, 0xbf, 0xcc, 0x46, 0xb2, 0x07,
+	0xcd, 0x9d, 0xf8, 0x7b, 0x3c, 0x19, 0x77, 0xc9, 0xff, 0x54, 0x3b, 0x85, 0x2f, 0xf3, 0xd6, 0x46,
+	0x39, 0x33, 0x8a, 0xc8, 0x58, 0x20, 0x5f, 0xc0, 0x6a, 0x7e, 0x04, 0x27, 0x57, 0x55, 0x8d, 0xd2,
+	0xaf, 0x82, 0x96, 0x71, 0x9a, 0x48, 0x6a, 0xfa, 0x1e, 0xac, 0x24, 0xa3, 0x6c, 0x3e, 0xc6, 0xc2,
+	0x80, 0xdb, 0x6a, 0xaa, 0x4c, 0xc1, 0x30, 0x16, 0xc8, 0xc7, 0x91, 0xb2, 0x18, 0xcb, 0x66, 0x95,
+	0x95, 0x99, 0xb3, 0x75, 0xa1, 0x64, 0xc0, 0x33, 0x16, 0xc8, 0x73, 0x38, 0xbf, 0x23, 0x2b, 0x74,
+	0xdc, 0xbc, 0xc9, 0xff, 0xf3, 0x4e, 0x4e, 0x98, 0xd9, 0xf2, 0x47, 0x2b, 0xef, 0xff, 0xc6, 0x02,
+	0xf9, 0x49, 0x83, 0x0b, 0x3b, 0x18, 0x16, 0x7b, 0x21, 0xb9, 0x51, 0xee, 0xe4, 0x84, 0x9e, 0xd9,
+	0x7a, 0x3a, 0x2f, 0x66, 0xf3, 0x66, 0x8d, 0x05, 0xb2, 0x2f, 0x8f, 0x9d, 0x61, 0x8f, 0x5c, 0x29,
+	0x05, 0x59, 0x9a, 0xbd, 0xf6, 0x49, 0xec, 0xe4, 0xa8, 0x9f, 0xf4, 0x7e, 0x7f, 0xdd, 0xd6, 0xfe,
+	0x78, 0xdd, 0xd6, 0xfe, 0x7e, 0xdd, 0xd6, 0xbe, 0xbc, 0xf5, 0x2f, 0x3f, 0x25, 0x29, 0xbf, 0x7a,
+	0x51, 0xce, 0xec, 0x11, 0x43, 0x37, 0x3c, 0x5c, 0x92, 0x3f, 0x1c, 0xdd, 0xfa, 0x27, 0x00, 0x00,
+	0xff, 0xff, 0xcb, 0x19, 0x01, 0xb8, 0x14, 0x13, 0x00, 0x00,
 }
 
 // Reference imports to suppress errors if they are not otherwise used.
@@ -1766,6 +1775,18 @@ func (m *ManifestRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 		i -= len(m.XXX_unrecognized)
 		copy(dAtA[i:], m.XXX_unrecognized)
 	}
+	if m.NoRevisionCache {
+		i--
+		if m.NoRevisionCache {
+			dAtA[i] = 1
+		} else {
+			dAtA[i] = 0
+		}
+		i--
+		dAtA[i] = 0x1
+		i--
+		dAtA[i] = 0x90
+	}
 	if len(m.HelmRepoCreds) > 0 {
 		for iNdEx := len(m.HelmRepoCreds) - 1; iNdEx >= 0; iNdEx-- {
 			{
@@ -2971,6 +2992,9 @@ func (m *ManifestRequest) Size() (n int) {
 			n += 2 + l + sovRepository(uint64(l))
 		}
 	}
+	if m.NoRevisionCache {
+		n += 3
+	}
 	if m.XXX_unrecognized != nil {
 		n += len(m.XXX_unrecognized)
 	}
@@ -3895,6 +3919,26 @@ func (m *ManifestRequest) Unmarshal(dAtA []byte) error {
 				return err
 			}
 			iNdEx = postIndex
+		case 18:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field NoRevisionCache", wireType)
+			}
+			var v int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowRepository
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				v |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			m.NoRevisionCache = bool(v != 0)
 		default:
 			iNdEx = preIndex
 			skippy, err := skipRepository(dAtA[iNdEx:])
diff --git a/reposerver/cache/cache.go b/reposerver/cache/cache.go
index 65ebbec95..2b7e5dc1b 100644
--- a/reposerver/cache/cache.go
+++ b/reposerver/cache/cache.go
@@ -5,34 +5,39 @@ import (
 	"encoding/json"
 	"fmt"
 	"hash/fnv"
+	"math"
 	"time"
 
+	"github.com/go-git/go-git/v5/plumbing"
 	"github.com/go-redis/redis/v8"
+	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 
 	appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
 	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
 	cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
+	"github.com/argoproj/argo-cd/v2/util/env"
 	"github.com/argoproj/argo-cd/v2/util/hash"
-
-	log "github.com/sirupsen/logrus"
 )
 
 var ErrCacheMiss = cacheutil.ErrCacheMiss
 
 type Cache struct {
-	cache               *cacheutil.Cache
-	repoCacheExpiration time.Duration
+	cache                   *cacheutil.Cache
+	repoCacheExpiration     time.Duration
+	revisionCacheExpiration time.Duration
 }
 
-func NewCache(cache *cacheutil.Cache, repoCacheExpiration time.Duration) *Cache {
-	return &Cache{cache, repoCacheExpiration}
+func NewCache(cache *cacheutil.Cache, repoCacheExpiration time.Duration, revisionCacheExpiration time.Duration) *Cache {
+	return &Cache{cache, repoCacheExpiration, revisionCacheExpiration}
 }
 
 func AddCacheFlagsToCmd(cmd *cobra.Command, opts ...func(client *redis.Client)) func() (*Cache, error) {
 	var repoCacheExpiration time.Duration
+	var revisionCacheExpiration time.Duration
 
 	cmd.Flags().DurationVar(&repoCacheExpiration, "repo-cache-expiration", 24*time.Hour, "Cache expiration for repo state, incl. app lists, app details, manifest generation, revision meta-data")
+	cmd.Flags().DurationVar(&revisionCacheExpiration, "revision-cache-expiration", env.ParseDurationFromEnv("ARGOCD_RECONCILIATION_TIMEOUT", 3*time.Minute, 0, math.MaxInt32), "Cache expiration for cached revision")
 
 	repoFactory := cacheutil.AddCacheFlagsToCmd(cmd, opts...)
 
@@ -41,7 +46,7 @@ func AddCacheFlagsToCmd(cmd *cobra.Command, opts ...func(client *redis.Client))
 		if err != nil {
 			return nil, err
 		}
-		return NewCache(cache, repoCacheExpiration), nil
+		return NewCache(cache, repoCacheExpiration, revisionCacheExpiration), nil
 	}
 }
 
@@ -69,6 +74,47 @@ func (c *Cache) SetApps(repoUrl, revision string, apps map[string]string) error
 	return c.cache.SetItem(listApps(repoUrl, revision), apps, c.repoCacheExpiration, apps == nil)
 }
 
+func helmIndexRefsKey(repo string) string {
+	return fmt.Sprintf("helm-index|%s", repo)
+}
+
+// SetHelmIndex stores helm repository index.yaml content to cache
+func (c *Cache) SetHelmIndex(repo string, indexData []byte) error {
+	return c.cache.SetItem(helmIndexRefsKey(repo), indexData, c.revisionCacheExpiration, false)
+}
+
+// GetHelmIndex retrieves helm repository index.yaml content from cache
+func (c *Cache) GetHelmIndex(repo string, indexData *[]byte) error {
+	return c.cache.GetItem(helmIndexRefsKey(repo), indexData)
+}
+
+func gitRefsKey(repo string) string {
+	return fmt.Sprintf("git-refs|%s", repo)
+}
+
+// SetGitReferences saves resolved Git repository references to cache
+func (c *Cache) SetGitReferences(repo string, references []*plumbing.Reference) error {
+	var input [][2]string
+	for i := range references {
+		input = append(input, references[i].Strings())
+	}
+	return c.cache.SetItem(gitRefsKey(repo), input, c.revisionCacheExpiration, false)
+}
+
+// GetGitReferences retrieves resolved Git repository references from cache
+func (c *Cache) GetGitReferences(repo string, references *[]*plumbing.Reference) error {
+	var input [][2]string
+	if err := c.cache.GetItem(gitRefsKey(repo), &input); err != nil {
+		return err
+	}
+	var res []*plumbing.Reference
+	for i := range input {
+		res = append(res, plumbing.NewReferenceFromStrings(input[i][0], input[i][1]))
+	}
+	*references = res
+	return nil
+}
+
 func manifestCacheKey(revision string, appSrc *appv1.ApplicationSource, namespace string, appLabelKey string, appName string) string {
 	return fmt.Sprintf("mfst|%s|%s|%s|%s|%d", appLabelKey, appName, revision, namespace, appSourceKey(appSrc))
 }
diff --git a/reposerver/metrics/githandlers.go b/reposerver/metrics/githandlers.go
new file mode 100644
index 000000000..cce632cf5
--- /dev/null
+++ b/reposerver/metrics/githandlers.go
@@ -0,0 +1,27 @@
+package metrics
+
+import (
+	"time"
+
+	"github.com/argoproj/argo-cd/v2/util/git"
+)
+
+// NewGitClientEventHandlers creates event handlers that update Git related metrics
+func NewGitClientEventHandlers(metricsServer *MetricsServer) git.EventHandlers {
+	return git.EventHandlers{
+		OnFetch: func(repo string) func() {
+			startTime := time.Now()
+			metricsServer.IncGitRequest(repo, GitRequestTypeFetch)
+			return func() {
+				metricsServer.ObserveGitRequestDuration(repo, GitRequestTypeFetch, time.Since(startTime))
+			}
+		},
+		OnLsRemote: func(repo string) func() {
+			startTime := time.Now()
+			metricsServer.IncGitRequest(repo, GitRequestTypeLsRemote)
+			return func() {
+				metricsServer.ObserveGitRequestDuration(repo, GitRequestTypeLsRemote, time.Since(startTime))
+			}
+		},
+	}
+}
diff --git a/reposerver/metrics/gitwrapper.go b/reposerver/metrics/gitwrapper.go
deleted file mode 100644
index 59aa9736f..000000000
--- a/reposerver/metrics/gitwrapper.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package metrics
-
-import (
-	"time"
-
-	"github.com/argoproj/argo-cd/v2/util/git"
-)
-
-type gitClientWrapper struct {
-	repo          string
-	client        git.Client
-	metricsServer *MetricsServer
-}
-
-func WrapGitClient(repo string, metricsServer *MetricsServer, client git.Client) git.Client {
-	return &gitClientWrapper{repo: repo, client: client, metricsServer: metricsServer}
-}
-
-func (w *gitClientWrapper) Fetch(revision string) error {
-	startTime := time.Now()
-	w.metricsServer.IncGitRequest(w.repo, GitRequestTypeFetch)
-	defer w.metricsServer.ObserveGitRequestDuration(w.repo, GitRequestTypeFetch, time.Since(startTime))
-	return w.client.Fetch(revision)
-}
-
-func (w *gitClientWrapper) LsRemote(revision string) (string, error) {
-	startTime := time.Now()
-	sha, err := w.client.LsRemote(revision)
-	if sha != revision {
-		// This is true only if specified revision is a tag, branch or HEAD and client had to use 'ls-remote'
-		w.metricsServer.IncGitRequest(w.repo, GitRequestTypeLsRemote)
-		defer w.metricsServer.ObserveGitRequestDuration(w.repo, GitRequestTypeLsRemote, time.Since(startTime))
-	}
-	return sha, err
-}
-
-func (w *gitClientWrapper) LsRefs() (*git.Refs, error) {
-	return w.client.LsRefs()
-}
-
-func (w *gitClientWrapper) LsFiles(path string) ([]string, error) {
-	return w.client.LsFiles(path)
-}
-
-func (w *gitClientWrapper) LsLargeFiles() ([]string, error) {
-	return w.client.LsLargeFiles()
-}
-
-func (w *gitClientWrapper) Checkout(revision string) error {
-	return w.client.Checkout(revision)
-}
-
-func (w *gitClientWrapper) CommitSHA() (string, error) {
-	return w.client.CommitSHA()
-}
-
-func (w *gitClientWrapper) Root() string {
-	return w.client.Root()
-}
-
-func (w *gitClientWrapper) Init() error {
-	return w.client.Init()
-}
-
-func (w *gitClientWrapper) RevisionMetadata(revision string) (*git.RevisionMetadata, error) {
-	return w.client.RevisionMetadata(revision)
-}
-
-func (w *gitClientWrapper) VerifyCommitSignature(revision string) (string, error) {
-	return w.client.VerifyCommitSignature(revision)
-}
diff --git a/reposerver/repository/repository.go b/reposerver/repository/repository.go
index cce88f325..3f7e2caf0 100644
--- a/reposerver/repository/repository.go
+++ b/reposerver/repository/repository.go
@@ -68,8 +68,8 @@ type Service struct {
 	cache                     *reposervercache.Cache
 	parallelismLimitSemaphore *semaphore.Weighted
 	metricsServer             *metrics.MetricsServer
-	newGitClient              func(rawRepoURL string, creds git.Creds, insecure bool, enableLfs bool) (git.Client, error)
-	newHelmClient             func(repoURL string, creds helm.Creds, enableOci bool) helm.Client
+	newGitClient              func(rawRepoURL string, creds git.Creds, insecure bool, enableLfs bool, opts ...git.ClientOpts) (git.Client, error)
+	newHelmClient             func(repoURL string, creds helm.Creds, enableOci bool, opts ...helm.ClientOpts) helm.Client
 	initConstants             RepoServerInitConstants
 	// now is usually just time.Now, but may be replaced by unit tests for testing purposes
 	now func() time.Time
@@ -95,8 +95,8 @@ func NewService(metricsServer *metrics.MetricsServer, cache *reposervercache.Cac
 		cache:                     cache,
 		metricsServer:             metricsServer,
 		newGitClient:              git.NewClient,
-		newHelmClient: func(repoURL string, creds helm.Creds, enableOci bool) helm.Client {
-			return helm.NewClientWithLock(repoURL, creds, sync.NewKeyLock(), enableOci)
+		newHelmClient: func(repoURL string, creds helm.Creds, enableOci bool, opts ...helm.ClientOpts) helm.Client {
+			return helm.NewClientWithLock(repoURL, creds, sync.NewKeyLock(), enableOci, opts...)
 		},
 		initConstants: initConstants,
 		now:           time.Now,
@@ -164,6 +164,7 @@ func (s *Service) ListApps(ctx context.Context, q *apiclient.ListAppsRequest) (*
 type operationSettings struct {
 	sem             *semaphore.Weighted
 	noCache         bool
+	noRevisionCache bool
 	allowConcurrent bool
 }
 
@@ -203,12 +204,12 @@ func (s *Service) runRepoOperation(
 	var err error
 	revision = textutils.FirstNonEmpty(revision, source.TargetRevision)
 	if source.IsHelm() {
-		helmClient, revision, err = s.newHelmClientResolveRevision(repo, revision, source.Chart, settings.noCache)
+		helmClient, revision, err = s.newHelmClientResolveRevision(repo, revision, source.Chart, settings.noCache || settings.noRevisionCache)
 		if err != nil {
 			return err
 		}
 	} else {
-		gitClient, revision, err = s.newClientResolveRevision(repo, revision)
+		gitClient, revision, err = s.newClientResolveRevision(repo, revision, git.WithCache(s.cache, !settings.noRevisionCache && !settings.noCache))
 		if err != nil {
 			return err
 		}
@@ -302,7 +303,7 @@ func (s *Service) GenerateManifest(ctx context.Context, q *apiclient.ManifestReq
 		return err
 	}
 
-	settings := operationSettings{sem: s.parallelismLimitSemaphore, noCache: q.NoCache, allowConcurrent: q.ApplicationSource.AllowsConcurrentProcessing()}
+	settings := operationSettings{sem: s.parallelismLimitSemaphore, noCache: q.NoCache, noRevisionCache: q.NoRevisionCache, allowConcurrent: q.ApplicationSource.AllowsConcurrentProcessing()}
 
 	err = s.runRepoOperation(ctx, q.Revision, q.Repo, q.ApplicationSource, q.VerifySignature, cacheFn, operation, settings)
 
@@ -1390,18 +1391,15 @@ func fileParameters(q *apiclient.RepoServerAppDetailsQuery) []v1alpha1.HelmFileP
 	return q.Source.Helm.FileParameters
 }
 
-func (s *Service) newClient(repo *v1alpha1.Repository) (git.Client, error) {
-	gitClient, err := s.newGitClient(repo.Repo, repo.GetGitCreds(), repo.IsInsecure(), repo.EnableLFS)
-	if err != nil {
-		return nil, err
-	}
-	return metrics.WrapGitClient(repo.Repo, s.metricsServer, gitClient), nil
+func (s *Service) newClient(repo *v1alpha1.Repository, opts ...git.ClientOpts) (git.Client, error) {
+	opts = append(opts, git.WithEventHandlers(metrics.NewGitClientEventHandlers(s.metricsServer)))
+	return s.newGitClient(repo.Repo, repo.GetGitCreds(), repo.IsInsecure(), repo.EnableLFS, opts...)
 }
 
 // newClientResolveRevision is a helper to perform the common task of instantiating a git client
 // and resolving a revision to a commit SHA
-func (s *Service) newClientResolveRevision(repo *v1alpha1.Repository, revision string) (git.Client, string, error) {
-	gitClient, err := s.newClient(repo)
+func (s *Service) newClientResolveRevision(repo *v1alpha1.Repository, revision string, opts ...git.ClientOpts) (git.Client, string, error) {
+	gitClient, err := s.newClient(repo, opts...)
 	if err != nil {
 		return nil, "", err
 	}
@@ -1412,9 +1410,9 @@ func (s *Service) newClientResolveRevision(repo *v1alpha1.Repository, revision s
 	return gitClient, commitSHA, nil
 }
 
-func (s *Service) newHelmClientResolveRevision(repo *v1alpha1.Repository, revision string, chart string, noCache bool) (helm.Client, string, error) {
+func (s *Service) newHelmClientResolveRevision(repo *v1alpha1.Repository, revision string, chart string, noRevisionCache bool) (helm.Client, string, error) {
 	enableOCI := repo.EnableOCI || helm.IsHelmOciRepo(repo.Repo)
-	helmClient := s.newHelmClient(repo.Repo, repo.GetHelmCreds(), enableOCI)
+	helmClient := s.newHelmClient(repo.Repo, repo.GetHelmCreds(), enableOCI, helm.WithIndexCache(s.cache))
 	// OCI helm registers don't support semver ranges. Assuming that given revision is exact version
 	if helm.IsVersion(revision) || enableOCI {
 		return helmClient, revision, nil
@@ -1423,7 +1421,7 @@ func (s *Service) newHelmClientResolveRevision(repo *v1alpha1.Repository, revisi
 	if err != nil {
 		return nil, "", fmt.Errorf("invalid revision '%s': %v", revision, err)
 	}
-	index, err := helmClient.GetIndex(noCache)
+	index, err := helmClient.GetIndex(noRevisionCache)
 	if err != nil {
 		return nil, "", err
 	}
diff --git a/reposerver/repository/repository.proto b/reposerver/repository/repository.proto
index c49ac1e46..03f64011b 100644
--- a/reposerver/repository/repository.proto
+++ b/reposerver/repository/repository.proto
@@ -27,6 +27,7 @@ message ManifestRequest {
     // Request to verify the signature when generating the manifests (only for Git repositories)
     bool verifySignature = 16;
     repeated github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.RepoCreds helmRepoCreds = 17;
+    bool noRevisionCache = 18;
 }
 
 // TestRepositoryRequest is a query to test repository is valid or not and has valid access.
diff --git a/server/application/application.go b/server/application/application.go
index d878457e1..f9b327f8c 100644
--- a/server/application/application.go
+++ b/server/application/application.go
@@ -396,7 +396,7 @@ func (s *Server) Get(ctx context.Context, q *application.ApplicationQuery) (*app
 				if annotations == nil {
 					annotations = make(map[string]string)
 				}
-				if _, ok := annotations[argocommon.AnnotationKeyRefresh]; !ok {
+				if _, ok := annotations[appv1.AnnotationKeyRefresh]; !ok {
 					return &event.Application, nil
 				}
 			}
@@ -1787,11 +1787,11 @@ func convertSyncWindows(w *v1alpha1.SyncWindows) []*application.ApplicationSyncW
 func getPropagationPolicyFinalizer(policy string) string {
 	switch strings.ToLower(policy) {
 	case backgroundPropagationPolicy:
-		return argocommon.BackgroundPropagationPolicyFinalizer
+		return appv1.BackgroundPropagationPolicyFinalizer
 	case foregroundPropagationPolicy:
-		return argocommon.ForegroundPropagationPolicyFinalizer
+		return appv1.ForegroundPropagationPolicyFinalizer
 	case "":
-		return argocommon.ResourcesFinalizerName
+		return appv1.ResourcesFinalizerName
 	default:
 		return ""
 	}
diff --git a/server/project/project.go b/server/project/project.go
index 70d0d5316..ec94fe4a8 100644
--- a/server/project/project.go
+++ b/server/project/project.go
@@ -19,7 +19,6 @@ import (
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/tools/cache"
 
-	"github.com/argoproj/argo-cd/v2/common"
 	"github.com/argoproj/argo-cd/v2/pkg/apiclient/project"
 	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
 	appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned"
@@ -360,7 +359,7 @@ func (s *Server) Update(ctx context.Context, q *project.ProjectUpdateRequest) (*
 
 // Delete deletes a project
 func (s *Server) Delete(ctx context.Context, q *project.ProjectQuery) (*project.EmptyResponse, error) {
-	if q.Name == common.DefaultAppProjectName {
+	if q.Name == v1alpha1.DefaultAppProjectName {
 		return nil, status.Errorf(codes.InvalidArgument, "name '%s' is reserved and cannot be deleted", q.Name)
 	}
 	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceProjects, rbacpolicy.ActionDelete, q.Name); err != nil {
diff --git a/server/server.go b/server/server.go
index fde5a5fce..a0ed31f76 100644
--- a/server/server.go
+++ b/server/server.go
@@ -186,7 +186,7 @@ type ArgoCDServerOpts struct {
 // initializeDefaultProject creates the default project if it does not already exist
 func initializeDefaultProject(opts ArgoCDServerOpts) error {
 	defaultProj := &v1alpha1.AppProject{
-		ObjectMeta: metav1.ObjectMeta{Name: common.DefaultAppProjectName, Namespace: opts.Namespace},
+		ObjectMeta: metav1.ObjectMeta{Name: v1alpha1.DefaultAppProjectName, Namespace: opts.Namespace},
 		Spec: v1alpha1.AppProjectSpec{
 			SourceRepos:              []string{"*"},
 			Destinations:             []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}},
@@ -725,8 +725,8 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl
 	// Dex reverse proxy and client app and OAuth2 login/callback
 	a.registerDexHandlers(mux)
 
-	// Webhook handler for git events
-	acdWebhookHandler := webhook.NewHandler(a.Namespace, a.AppClientset, a.settings, a.settingsMgr, repocache.NewCache(a.Cache.GetCache(), 24*time.Hour))
+	// Webhook handler for git events (Note: cache timeouts are hardcoded because API server does not write to cache and not really using them)
+	acdWebhookHandler := webhook.NewHandler(a.Namespace, a.AppClientset, a.settings, a.settingsMgr, repocache.NewCache(a.Cache.GetCache(), 24*time.Hour, 3*time.Minute))
 	mux.HandleFunc("/api/webhook", acdWebhookHandler.Handler)
 
 	// Serve cli binaries directly from API server
diff --git a/util/argo/argo.go b/util/argo/argo.go
index d1a1b12bf..95bd3ea17 100644
--- a/util/argo/argo.go
+++ b/util/argo/argo.go
@@ -18,7 +18,6 @@ import (
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/watch"
 
-	"github.com/argoproj/argo-cd/v2/common"
 	argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
 	"github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned/typed/application/v1alpha1"
 	applicationsv1 "github.com/argoproj/argo-cd/v2/pkg/client/listers/application/v1alpha1"
@@ -95,7 +94,7 @@ func RefreshApp(appIf v1alpha1.ApplicationInterface, name string, refreshType ar
 	metadata := map[string]interface{}{
 		"metadata": map[string]interface{}{
 			"annotations": map[string]string{
-				common.AnnotationKeyRefresh: string(refreshType),
+				argoappv1.AnnotationKeyRefresh: string(refreshType),
 			},
 		},
 	}
@@ -144,7 +143,7 @@ func WaitForRefresh(ctx context.Context, appIf v1alpha1.ApplicationInterface, na
 		if annotations == nil {
 			annotations = make(map[string]string)
 		}
-		if _, ok := annotations[common.AnnotationKeyRefresh]; !ok {
+		if _, ok := annotations[argoappv1.AnnotationKeyRefresh]; !ok {
 			return app, nil
 		}
 	}
@@ -480,7 +479,7 @@ func ContainsSyncResource(name string, namespace string, gvk schema.GroupVersion
 func NormalizeApplicationSpec(spec *argoappv1.ApplicationSpec) *argoappv1.ApplicationSpec {
 	spec = spec.DeepCopy()
 	if spec.Project == "" {
-		spec.Project = common.DefaultAppProjectName
+		spec.Project = argoappv1.DefaultAppProjectName
 	}
 
 	// 3. If any app sources are their zero values, then nil out the pointers to the source spec.
diff --git a/util/db/cluster.go b/util/db/cluster.go
index 446cba82a..eb85d64fe 100644
--- a/util/db/cluster.go
+++ b/util/db/cluster.go
@@ -32,7 +32,7 @@ import (
 var (
 	localCluster = appv1.Cluster{
 		Name:            "in-cluster",
-		Server:          common.KubernetesInternalAPIServerAddr,
+		Server:          appv1.KubernetesInternalAPIServerAddr,
 		ConnectionState: appv1.ConnectionState{Status: appv1.ConnectionStatusSuccessful},
 	}
 	initLocalCluster sync.Once
@@ -89,7 +89,7 @@ func (db *db) ListClusters(ctx context.Context) (*appv1.ClusterList, error) {
 	for i, clusterSecret := range clusterSecrets {
 		cluster := *secretToCluster(clusterSecret)
 		clusterList.Items[i] = cluster
-		if cluster.Server == common.KubernetesInternalAPIServerAddr {
+		if cluster.Server == appv1.KubernetesInternalAPIServerAddr {
 			hasInClusterCredentials = true
 		}
 	}
@@ -140,7 +140,7 @@ func (db *db) WatchClusters(ctx context.Context,
 	handleAddEvent func(cluster *appv1.Cluster),
 	handleModEvent func(oldCluster *appv1.Cluster, newCluster *appv1.Cluster),
 	handleDeleteEvent func(clusterServer string)) error {
-	localCls, err := db.GetCluster(ctx, common.KubernetesInternalAPIServerAddr)
+	localCls, err := db.GetCluster(ctx, appv1.KubernetesInternalAPIServerAddr)
 	if err != nil {
 		return err
 	}
@@ -153,7 +153,7 @@ func (db *db) WatchClusters(ctx context.Context,
 		AddFunc: func(obj interface{}) {
 			if secretObj, ok := obj.(*apiv1.Secret); ok {
 				cluster := secretToCluster(secretObj)
-				if cluster.Server == common.KubernetesInternalAPIServerAddr {
+				if cluster.Server == appv1.KubernetesInternalAPIServerAddr {
 					// change local cluster event to modified or deleted, since it cannot be re-added or deleted
 					handleModEvent(localCls, cluster)
 					localCls = cluster
@@ -164,7 +164,7 @@ func (db *db) WatchClusters(ctx context.Context,
 		},
 		DeleteFunc: func(obj interface{}) {
 			if secretObj, ok := obj.(*apiv1.Secret); ok {
-				if string(secretObj.Data["server"]) == common.KubernetesInternalAPIServerAddr {
+				if string(secretObj.Data["server"]) == appv1.KubernetesInternalAPIServerAddr {
 					// change local cluster event to modified or deleted, since it cannot be re-added or deleted
 					handleModEvent(localCls, db.getLocalCluster())
 					localCls = db.getLocalCluster()
@@ -178,7 +178,7 @@ func (db *db) WatchClusters(ctx context.Context,
 				if newSecretObj, ok := newObj.(*apiv1.Secret); ok {
 					oldCluster := secretToCluster(oldSecretObj)
 					newCluster := secretToCluster(newSecretObj)
-					if newCluster.Server == common.KubernetesInternalAPIServerAddr {
+					if newCluster.Server == appv1.KubernetesInternalAPIServerAddr {
 						localCls = newCluster
 					}
 					handleModEvent(oldCluster, newCluster)
@@ -216,7 +216,7 @@ func (db *db) getClusterSecret(server string) (*apiv1.Secret, error) {
 func (db *db) GetCluster(ctx context.Context, server string) (*appv1.Cluster, error) {
 	clusterSecret, err := db.getClusterSecret(server)
 	if err != nil {
-		if errorStatus, ok := status.FromError(err); ok && errorStatus.Code() == codes.NotFound && server == common.KubernetesInternalAPIServerAddr {
+		if errorStatus, ok := status.FromError(err); ok && errorStatus.Code() == codes.NotFound && server == appv1.KubernetesInternalAPIServerAddr {
 			return db.getLocalCluster(), nil
 		} else {
 			return nil, err
@@ -305,9 +305,9 @@ func clusterToSecret(c *appv1.Cluster, secret *apiv1.Secret) error {
 		secret.Annotations = make(map[string]string)
 	}
 	if c.RefreshRequestedAt != nil {
-		secret.Annotations[common.AnnotationKeyRefresh] = c.RefreshRequestedAt.Format(time.RFC3339)
+		secret.Annotations[appv1.AnnotationKeyRefresh] = c.RefreshRequestedAt.Format(time.RFC3339)
 	} else {
-		delete(secret.Annotations, common.AnnotationKeyRefresh)
+		delete(secret.Annotations, appv1.AnnotationKeyRefresh)
 	}
 	return nil
 }
@@ -329,7 +329,7 @@ func secretToCluster(s *apiv1.Secret) *appv1.Cluster {
 		}
 	}
 	var refreshRequestedAt *metav1.Time
-	if v, found := s.Annotations[common.AnnotationKeyRefresh]; found {
+	if v, found := s.Annotations[appv1.AnnotationKeyRefresh]; found {
 		requestedAt, err := time.Parse(time.RFC3339, v)
 		if err != nil {
 			log.Warnf("Error while parsing date in cluster secret '%s': %v", s.Name, err)
diff --git a/util/git/client.go b/util/git/client.go
index 162281587..162b54113 100644
--- a/util/git/client.go
+++ b/util/git/client.go
@@ -43,6 +43,11 @@ type Refs struct {
 	// heads and remotes are also refs, but are not needed at this time.
 }
 
+type gitRefCache interface {
+	SetGitReferences(repo string, references []*plumbing.Reference) error
+	GetGitReferences(repo string, references *[]*plumbing.Reference) error
+}
+
 // Client is a generic git client interface
 type Client interface {
 	Root() string
@@ -58,8 +63,15 @@ type Client interface {
 	VerifyCommitSignature(string) (string, error)
 }
 
+type EventHandlers struct {
+	OnLsRemote func(repo string) func()
+	OnFetch    func(repo string) func()
+}
+
 // nativeGitClient implements Client interface using git CLI
 type nativeGitClient struct {
+	EventHandlers
+
 	// URL of the repository
 	repoURL string
 	// Root path of repository
@@ -70,6 +82,10 @@ type nativeGitClient struct {
 	insecure bool
 	// Whether the repository is LFS enabled
 	enableLfs bool
+	// gitRefCache knows how to cache git refs
+	gitRefCache gitRefCache
+	// indicates if client allowed to load refs from cache
+	loadRefFromCache bool
 }
 
 var (
@@ -86,23 +102,43 @@ func init() {
 	}
 }
 
-func NewClient(rawRepoURL string, creds Creds, insecure bool, enableLfs bool) (Client, error) {
+type ClientOpts func(c *nativeGitClient)
+
+// WithCache sets git revisions cacher as well as specifies if client should tries to use cached resolved revision
+func WithCache(cache gitRefCache, loadRefFromCache bool) ClientOpts {
+	return func(c *nativeGitClient) {
+		c.gitRefCache = cache
+		c.loadRefFromCache = loadRefFromCache
+	}
+}
+
+// WithEventHandlers sets the git client event handlers
+func WithEventHandlers(handlers EventHandlers) ClientOpts {
+	return func(c *nativeGitClient) {
+		c.EventHandlers = handlers
+	}
+}
+
+func NewClient(rawRepoURL string, creds Creds, insecure bool, enableLfs bool, opts ...ClientOpts) (Client, error) {
 	root := filepath.Join(os.TempDir(), strings.Replace(NormalizeGitURL(rawRepoURL), "/", "_", -1))
 	if root == os.TempDir() {
 		return nil, fmt.Errorf("Repository '%s' cannot be initialized, because its root would be system temp at %s", rawRepoURL, root)
 	}
-	return NewClientExt(rawRepoURL, root, creds, insecure, enableLfs)
+	return NewClientExt(rawRepoURL, root, creds, insecure, enableLfs, opts...)
 }
 
-func NewClientExt(rawRepoURL string, root string, creds Creds, insecure bool, enableLfs bool) (Client, error) {
-	client := nativeGitClient{
+func NewClientExt(rawRepoURL string, root string, creds Creds, insecure bool, enableLfs bool, opts ...ClientOpts) (Client, error) {
+	client := &nativeGitClient{
 		repoURL:   rawRepoURL,
 		root:      root,
 		creds:     creds,
 		insecure:  insecure,
 		enableLfs: enableLfs,
 	}
-	return &client, nil
+	for i := range opts {
+		opts[i](client)
+	}
+	return client, nil
 }
 
 // Returns a HTTP client object suitable for go-git to use using the following
@@ -270,6 +306,11 @@ func (m *nativeGitClient) IsLFSEnabled() bool {
 
 // Fetch fetches latest updates from origin
 func (m *nativeGitClient) Fetch(revision string) error {
+	if m.OnFetch != nil {
+		done := m.OnFetch(m.repoURL)
+		defer done()
+	}
+
 	var err error
 	if revision != "" {
 		err = m.runCredentialedCmd("git", "fetch", "origin", revision, "--tags", "--force")
@@ -345,6 +386,18 @@ func (m *nativeGitClient) Checkout(revision string) error {
 }
 
 func (m *nativeGitClient) getRefs() ([]*plumbing.Reference, error) {
+	if m.gitRefCache != nil && m.loadRefFromCache {
+		var res []*plumbing.Reference
+		if m.gitRefCache.GetGitReferences(m.repoURL, &res) == nil {
+			return res, nil
+		}
+	}
+
+	if m.OnLsRemote != nil {
+		done := m.OnLsRemote(m.repoURL)
+		defer done()
+	}
+
 	repo, err := git.Init(memory.NewStorage(), nil)
 	if err != nil {
 		return nil, err
@@ -360,7 +413,14 @@ func (m *nativeGitClient) getRefs() ([]*plumbing.Reference, error) {
 	if err != nil {
 		return nil, err
 	}
-	return listRemote(remote, &git.ListOptions{Auth: auth}, m.insecure, m.creds)
+	res, err := listRemote(remote, &git.ListOptions{Auth: auth}, m.insecure, m.creds)
+	if err == nil && m.gitRefCache != nil {
+		if err := m.gitRefCache.SetGitReferences(m.repoURL, res); err != nil {
+			log.Warnf("Failed to store git references to cache: %v", err)
+		}
+		return res, nil
+	}
+	return res, err
 }
 
 func (m *nativeGitClient) LsRefs() (*Refs, error) {
diff --git a/util/helm/client.go b/util/helm/client.go
index fa330a038..b95f7abec 100644
--- a/util/helm/client.go
+++ b/util/helm/client.go
@@ -20,18 +20,14 @@ import (
 	log "github.com/sirupsen/logrus"
 	"gopkg.in/yaml.v2"
 
-	"github.com/argoproj/argo-cd/v2/common"
 	"github.com/argoproj/argo-cd/v2/util/cache"
-	"github.com/argoproj/argo-cd/v2/util/env"
 	executil "github.com/argoproj/argo-cd/v2/util/exec"
 	"github.com/argoproj/argo-cd/v2/util/io"
 )
 
 var (
-	globalLock    = sync.NewKeyLock()
-	indexDuration = env.ParseDurationFromEnv(common.EnvHelmIndexCacheDuration, 0, 0, time.Hour*24)
-	indexCache    = cache.NewInMemoryCache(indexDuration)
-	indexLock     = sync.NewKeyLock()
+	globalLock = sync.NewKeyLock()
+	indexLock  = sync.NewKeyLock()
 )
 
 type Creds struct {
@@ -43,6 +39,11 @@ type Creds struct {
 	InsecureSkipVerify bool
 }
 
+type indexCache interface {
+	SetHelmIndex(repo string, indexData []byte) error
+	GetHelmIndex(repo string, indexData *[]byte) error
+}
+
 type Client interface {
 	CleanChartCache(chart string, version string) error
 	ExtractChart(chart string, version string) (string, io.Closer, error)
@@ -50,28 +51,41 @@ type Client interface {
 	TestHelmOCI() (bool, error)
 }
 
-func NewClient(repoURL string, creds Creds, enableOci bool) Client {
-	return NewClientWithLock(repoURL, creds, globalLock, enableOci)
+type ClientOpts func(c *nativeHelmChart)
+
+func WithIndexCache(indexCache indexCache) ClientOpts {
+	return func(c *nativeHelmChart) {
+		c.indexCache = indexCache
+	}
+}
+
+func NewClient(repoURL string, creds Creds, enableOci bool, opts ...ClientOpts) Client {
+	return NewClientWithLock(repoURL, creds, globalLock, enableOci, opts...)
 }
 
-func NewClientWithLock(repoURL string, creds Creds, repoLock sync.KeyLock, enableOci bool) Client {
-	return &nativeHelmChart{
+func NewClientWithLock(repoURL string, creds Creds, repoLock sync.KeyLock, enableOci bool, opts ...ClientOpts) Client {
+	c := &nativeHelmChart{
 		repoURL:   repoURL,
 		creds:     creds,
 		repoPath:  filepath.Join(os.TempDir(), strings.Replace(repoURL, "/", "_", -1)),
 		repoLock:  repoLock,
 		enableOci: enableOci,
 	}
+	for i := range opts {
+		opts[i](c)
+	}
+	return c
 }
 
 var _ Client = &nativeHelmChart{}
 
 type nativeHelmChart struct {
-	repoPath  string
-	repoURL   string
-	creds     Creds
-	repoLock  sync.KeyLock
-	enableOci bool
+	repoPath   string
+	repoURL    string
+	creds      Creds
+	repoLock   sync.KeyLock
+	enableOci  bool
+	indexCache indexCache
 }
 
 func fileExist(filePath string) (bool, error) {
@@ -214,9 +228,8 @@ func (c *nativeHelmChart) GetIndex(noCache bool) (*Index, error) {
 	defer indexLock.Unlock(c.repoURL)
 
 	var data []byte
-	useCache := !noCache && indexDuration > 0
-	if useCache {
-		if err := indexCache.Get(c.repoURL, &data); err != nil && err != cache.ErrCacheMiss {
+	if !noCache && c.indexCache != nil {
+		if err := c.indexCache.GetHelmIndex(c.repoURL, &data); err != nil && err != cache.ErrCacheMiss {
 			log.Warnf("Failed to load index cache for repo: %s: %v", c.repoURL, err)
 		}
 	}
@@ -230,8 +243,10 @@ func (c *nativeHelmChart) GetIndex(noCache bool) (*Index, error) {
 		}
 		log.WithFields(log.Fields{"seconds": time.Since(start).Seconds()}).Info("took to get index")
 
-		if err := indexCache.Set(&cache.Item{Key: c.repoURL, Object: data}); err != nil {
-			log.Warnf("Failed to store index cache for repo: %s: %v", c.repoURL, err)
+		if c.indexCache != nil {
+			if err := c.indexCache.SetHelmIndex(c.repoURL, data); err != nil {
+				log.Warnf("Failed to store index cache for repo: %s: %v", c.repoURL, err)
+			}
 		}
 	}
 
diff --git a/util/webhook/webhook.go b/util/webhook/webhook.go
index 604465308..4d0406e64 100644
--- a/util/webhook/webhook.go
+++ b/util/webhook/webhook.go
@@ -19,7 +19,6 @@ import (
 	"gopkg.in/go-playground/webhooks.v5/gogs"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
-	"github.com/argoproj/argo-cd/v2/common"
 	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
 	appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned"
 	"github.com/argoproj/argo-cd/v2/reposerver/cache"
@@ -252,7 +251,7 @@ func (a *ArgoCDWebhookHandler) HandleEvent(payload interface{}) {
 
 func getAppRefreshPaths(app *v1alpha1.Application) []string {
 	var paths []string
-	if val, ok := app.Annotations[common.AnnotationKeyManifestGeneratePaths]; ok && val != "" {
+	if val, ok := app.Annotations[v1alpha1.AnnotationKeyManifestGeneratePaths]; ok && val != "" {
 		for _, item := range strings.Split(val, ";") {
 			if item == "" {
 				continue


Test patch:
diff --git a/cmd/argocd-util/commands/app_test.go b/cmd/argocd-util/commands/app_test.go
index 50ed5bee9..e14c1e4ee 100644
--- a/cmd/argocd-util/commands/app_test.go
+++ b/cmd/argocd-util/commands/app_test.go
@@ -16,7 +16,6 @@ import (
 	kubefake "k8s.io/client-go/kubernetes/fake"
 	"k8s.io/client-go/tools/cache"
 
-	"github.com/argoproj/argo-cd/v2/common"
 	statecache "github.com/argoproj/argo-cd/v2/controller/cache"
 	cachemocks "github.com/argoproj/argo-cd/v2/controller/cache/mocks"
 	"github.com/argoproj/argo-cd/v2/controller/metrics"
@@ -79,7 +78,7 @@ func TestGetReconcileResults_Refresh(t *testing.T) {
 		Spec: v1alpha1.ApplicationSpec{
 			Project: "default",
 			Destination: v1alpha1.ApplicationDestination{
-				Server:    common.KubernetesInternalAPIServerAddr,
+				Server:    v1alpha1.KubernetesInternalAPIServerAddr,
 				Namespace: "default",
 			},
 		},
diff --git a/controller/appcontroller_test.go b/controller/appcontroller_test.go
index d77878792..0c246f4d1 100644
--- a/controller/appcontroller_test.go
+++ b/controller/appcontroller_test.go
@@ -10,6 +10,7 @@ import (
 
 	clustercache "github.com/argoproj/gitops-engine/pkg/cache"
 
+	"github.com/argoproj/argo-cd/v2/common"
 	statecache "github.com/argoproj/argo-cd/v2/controller/cache"
 
 	"github.com/argoproj/gitops-engine/pkg/cache/mocks"
@@ -29,8 +30,8 @@ import (
 	kubetesting "k8s.io/client-go/testing"
 	"k8s.io/client-go/tools/cache"
 
-	"github.com/argoproj/argo-cd/v2/common"
 	mockstatecache "github.com/argoproj/argo-cd/v2/controller/cache/mocks"
+	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
 	argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
 	appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned/fake"
 	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
@@ -731,7 +732,7 @@ func TestNormalizeApplication(t *testing.T) {
 func TestHandleAppUpdated(t *testing.T) {
 	app := newFakeApp()
 	app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
-	app.Spec.Destination.Server = common.KubernetesInternalAPIServerAddr
+	app.Spec.Destination.Server = argoappv1.KubernetesInternalAPIServerAddr
 	ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
 
 	ctrl.handleObjectUpdated(map[string]bool{app.Name: true}, kube.GetObjectRef(kube.MustToUnstructured(app)))
@@ -749,12 +750,12 @@ func TestHandleOrphanedResourceUpdated(t *testing.T) {
 	app1 := newFakeApp()
 	app1.Name = "app1"
 	app1.Spec.Destination.Namespace = test.FakeArgoCDNamespace
-	app1.Spec.Destination.Server = common.KubernetesInternalAPIServerAddr
+	app1.Spec.Destination.Server = argoappv1.KubernetesInternalAPIServerAddr
 
 	app2 := newFakeApp()
 	app2.Name = "app2"
 	app2.Spec.Destination.Namespace = test.FakeArgoCDNamespace
-	app2.Spec.Destination.Server = common.KubernetesInternalAPIServerAddr
+	app2.Spec.Destination.Server = argoappv1.KubernetesInternalAPIServerAddr
 
 	proj := defaultProj.DeepCopy()
 	proj.Spec.OrphanedResources = &argoappv1.OrphanedResourcesMonitorSettings{}
@@ -855,7 +856,7 @@ func TestNeedRefreshAppStatus(t *testing.T) {
 	needRefresh, refreshType, compareWith = ctrl.needRefreshAppStatus(app, 1*time.Hour)
 	assert.True(t, needRefresh)
 	assert.Equal(t, argoappv1.RefreshTypeNormal, refreshType)
-	assert.Equal(t, CompareWithLatest, compareWith)
+	assert.Equal(t, CompareWithLatestForceResolve, compareWith)
 
 	{
 		// refresh app using the 'latest' level if comparison expired
@@ -866,7 +867,7 @@ func TestNeedRefreshAppStatus(t *testing.T) {
 		needRefresh, refreshType, compareWith = ctrl.needRefreshAppStatus(app, 1*time.Minute)
 		assert.True(t, needRefresh)
 		assert.Equal(t, argoappv1.RefreshTypeNormal, refreshType)
-		assert.Equal(t, CompareWithLatest, compareWith)
+		assert.Equal(t, CompareWithLatestForceResolve, compareWith)
 	}
 
 	{
@@ -875,12 +876,12 @@ func TestNeedRefreshAppStatus(t *testing.T) {
 		reconciledAt := metav1.NewTime(time.Now().UTC().Add(-1 * time.Hour))
 		app.Status.ReconciledAt = &reconciledAt
 		app.Annotations = map[string]string{
-			common.AnnotationKeyRefresh: string(argoappv1.RefreshTypeHard),
+			v1alpha1.AnnotationKeyRefresh: string(argoappv1.RefreshTypeHard),
 		}
 		needRefresh, refreshType, compareWith = ctrl.needRefreshAppStatus(app, 1*time.Hour)
 		assert.True(t, needRefresh)
 		assert.Equal(t, argoappv1.RefreshTypeHard, refreshType)
-		assert.Equal(t, CompareWithLatest, compareWith)
+		assert.Equal(t, CompareWithLatestForceResolve, compareWith)
 	}
 
 	{
@@ -898,7 +899,7 @@ func TestNeedRefreshAppStatus(t *testing.T) {
 		needRefresh, refreshType, compareWith = ctrl.needRefreshAppStatus(app, 1*time.Hour)
 		assert.True(t, needRefresh)
 		assert.Equal(t, argoappv1.RefreshTypeNormal, refreshType)
-		assert.Equal(t, CompareWithLatest, compareWith)
+		assert.Equal(t, CompareWithLatestForceResolve, compareWith)
 	}
 }
 
diff --git a/controller/state_test.go b/controller/state_test.go
index a7df7f162..9074cebf0 100644
--- a/controller/state_test.go
+++ b/controller/state_test.go
@@ -36,7 +36,7 @@ func TestCompareAppStateEmpty(t *testing.T) {
 		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
 	}
 	ctrl := newFakeController(&data)
-	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
+	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, false, nil)
 	assert.NotNil(t, compRes)
 	assert.NotNil(t, compRes.syncStatus)
 	assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
@@ -59,7 +59,7 @@ func TestCompareAppStateMissing(t *testing.T) {
 		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
 	}
 	ctrl := newFakeController(&data)
-	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
+	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, false, nil)
 	assert.NotNil(t, compRes)
 	assert.NotNil(t, compRes.syncStatus)
 	assert.Equal(t, argoappv1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
@@ -86,7 +86,7 @@ func TestCompareAppStateExtra(t *testing.T) {
 		},
 	}
 	ctrl := newFakeController(&data)
-	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
+	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, false, nil)
 	assert.NotNil(t, compRes)
 	assert.Equal(t, argoappv1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
 	assert.Equal(t, 1, len(compRes.resources))
@@ -112,7 +112,7 @@ func TestCompareAppStateHook(t *testing.T) {
 		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
 	}
 	ctrl := newFakeController(&data)
-	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
+	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, false, nil)
 	assert.NotNil(t, compRes)
 	assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
 	assert.Equal(t, 0, len(compRes.resources))
@@ -139,7 +139,7 @@ func TestCompareAppStateSkipHook(t *testing.T) {
 		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
 	}
 	ctrl := newFakeController(&data)
-	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
+	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, false, nil)
 	assert.NotNil(t, compRes)
 	assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
 	assert.Equal(t, 1, len(compRes.resources))
@@ -165,7 +165,7 @@ func TestCompareAppStateCompareOptionIgnoreExtraneous(t *testing.T) {
 	}
 	ctrl := newFakeController(&data)
 
-	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
+	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, false, nil)
 
 	assert.NotNil(t, compRes)
 	assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
@@ -193,7 +193,7 @@ func TestCompareAppStateExtraHook(t *testing.T) {
 		},
 	}
 	ctrl := newFakeController(&data)
-	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
+	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, false, nil)
 
 	assert.NotNil(t, compRes)
 	assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
@@ -236,7 +236,7 @@ func TestCompareAppStateDuplicatedNamespacedResources(t *testing.T) {
 		},
 	}
 	ctrl := newFakeController(&data)
-	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
+	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, false, nil)
 
 	assert.NotNil(t, compRes)
 	assert.Equal(t, 1, len(app.Status.Conditions))
@@ -287,7 +287,7 @@ func TestSetHealth(t *testing.T) {
 		},
 	})
 
-	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
+	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, false, nil)
 
 	assert.Equal(t, compRes.healthStatus.Status, health.HealthStatusHealthy)
 }
@@ -319,7 +319,7 @@ func TestSetHealthSelfReferencedApp(t *testing.T) {
 		},
 	})
 
-	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
+	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, false, nil)
 
 	assert.Equal(t, compRes.healthStatus.Status, health.HealthStatusHealthy)
 }
@@ -389,7 +389,7 @@ func TestReturnUnknownComparisonStateOnSettingLoadError(t *testing.T) {
 		},
 	})
 
-	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
+	compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, false, nil)
 
 	assert.Equal(t, health.HealthStatusUnknown, compRes.healthStatus.Status)
 	assert.Equal(t, argoappv1.SyncStatusCodeUnknown, compRes.syncStatus.Status)
@@ -525,7 +525,7 @@ func TestSignedResponseNoSignatureRequired(t *testing.T) {
 			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
 		}
 		ctrl := newFakeController(&data)
-		compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
+		compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, false, nil)
 		assert.NotNil(t, compRes)
 		assert.NotNil(t, compRes.syncStatus)
 		assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
@@ -547,7 +547,7 @@ func TestSignedResponseNoSignatureRequired(t *testing.T) {
 			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
 		}
 		ctrl := newFakeController(&data)
-		compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
+		compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, false, nil)
 		assert.NotNil(t, compRes)
 		assert.NotNil(t, compRes.syncStatus)
 		assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
@@ -576,7 +576,7 @@ func TestSignedResponseSignatureRequired(t *testing.T) {
 			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
 		}
 		ctrl := newFakeController(&data)
-		compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "", app.Spec.Source, false, nil)
+		compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "", app.Spec.Source, false, false, nil)
 		assert.NotNil(t, compRes)
 		assert.NotNil(t, compRes.syncStatus)
 		assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
@@ -598,7 +598,7 @@ func TestSignedResponseSignatureRequired(t *testing.T) {
 			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
 		}
 		ctrl := newFakeController(&data)
-		compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
+		compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, false, nil)
 		assert.NotNil(t, compRes)
 		assert.NotNil(t, compRes.syncStatus)
 		assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
@@ -620,7 +620,7 @@ func TestSignedResponseSignatureRequired(t *testing.T) {
 			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
 		}
 		ctrl := newFakeController(&data)
-		compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
+		compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, false, nil)
 		assert.NotNil(t, compRes)
 		assert.NotNil(t, compRes.syncStatus)
 		assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
@@ -642,7 +642,7 @@ func TestSignedResponseSignatureRequired(t *testing.T) {
 			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
 		}
 		ctrl := newFakeController(&data)
-		compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
+		compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, false, nil)
 		assert.NotNil(t, compRes)
 		assert.NotNil(t, compRes.syncStatus)
 		assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
@@ -667,7 +667,7 @@ func TestSignedResponseSignatureRequired(t *testing.T) {
 		ctrl := newFakeController(&data)
 		testProj := signedProj
 		testProj.Spec.SignatureKeys[0].KeyID = "4AEE18F83AFDEB24"
-		compRes := ctrl.appStateManager.CompareAppState(app, &testProj, "abc123", app.Spec.Source, false, nil)
+		compRes := ctrl.appStateManager.CompareAppState(app, &testProj, "abc123", app.Spec.Source, false, false, nil)
 		assert.NotNil(t, compRes)
 		assert.NotNil(t, compRes.syncStatus)
 		assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
@@ -692,7 +692,7 @@ func TestSignedResponseSignatureRequired(t *testing.T) {
 		// it doesn't matter for our test whether local manifests are valid
 		localManifests := []string{"foobar"}
 		ctrl := newFakeController(&data)
-		compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, localManifests)
+		compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, false, localManifests)
 		assert.NotNil(t, compRes)
 		assert.NotNil(t, compRes.syncStatus)
 		assert.Equal(t, argoappv1.SyncStatusCodeUnknown, compRes.syncStatus.Status)
@@ -717,7 +717,7 @@ func TestSignedResponseSignatureRequired(t *testing.T) {
 			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
 		}
 		ctrl := newFakeController(&data)
-		compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
+		compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, false, nil)
 		assert.NotNil(t, compRes)
 		assert.NotNil(t, compRes.syncStatus)
 		assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
@@ -742,7 +742,7 @@ func TestSignedResponseSignatureRequired(t *testing.T) {
 		// it doesn't matter for our test whether local manifests are valid
 		localManifests := []string{""}
 		ctrl := newFakeController(&data)
-		compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, localManifests)
+		compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, false, localManifests)
 		assert.NotNil(t, compRes)
 		assert.NotNil(t, compRes.syncStatus)
 		assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
diff --git a/pkg/apis/application/v1alpha1/types_test.go b/pkg/apis/application/v1alpha1/types_test.go
index 6dc90aa89..cd18fe8cc 100644
--- a/pkg/apis/application/v1alpha1/types_test.go
+++ b/pkg/apis/application/v1alpha1/types_test.go
@@ -11,8 +11,6 @@ import (
 	"github.com/stretchr/testify/assert"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime/schema"
-
-	argocommon "github.com/argoproj/argo-cd/v2/common"
 )
 
 func TestAppProject_IsSourcePermitted(t *testing.T) {
@@ -2180,9 +2178,9 @@ func TestUnSetCascadedDeletion(t *testing.T) {
 			Name: "test",
 			Finalizers: []string{
 				"alpha",
-				argocommon.ForegroundPropagationPolicyFinalizer,
+				ForegroundPropagationPolicyFinalizer,
 				"beta",
-				argocommon.BackgroundPropagationPolicyFinalizer,
+				BackgroundPropagationPolicyFinalizer,
 				"gamma",
 			},
 		},
diff --git a/reposerver/cache/cache_test.go b/reposerver/cache/cache_test.go
index 290134831..0bbbd14d1 100644
--- a/reposerver/cache/cache_test.go
+++ b/reposerver/cache/cache_test.go
@@ -24,6 +24,7 @@ func newFixtures() *fixtures {
 	return &fixtures{NewCache(
 		cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Hour)),
 		1*time.Minute,
+		1*time.Minute,
 	)}
 }
 
@@ -132,6 +133,7 @@ func TestCachedManifestResponse_HashBehavior(t *testing.T) {
 	repoCache := NewCache(
 		cacheutil.NewCache(inMemCache),
 		1*time.Minute,
+		1*time.Minute,
 	)
 
 	response := apiclient.ManifestResponse{
diff --git a/reposerver/repository/repository_test.go b/reposerver/repository/repository_test.go
index b3cf480dc..bf2b984bf 100644
--- a/reposerver/repository/repository_test.go
+++ b/reposerver/repository/repository_test.go
@@ -69,6 +69,7 @@ func newServiceWithOpt(cf clientFunc) (*Service, *gitmocks.Client) {
 	service := NewService(metrics.NewMetricsServer(), cache.NewCache(
 		cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Minute)),
 		1*time.Minute,
+		1*time.Minute,
 	), RepoServerInitConstants{ParallelismLimit: 1})
 
 	chart := "my-chart"
@@ -79,10 +80,10 @@ func newServiceWithOpt(cf clientFunc) (*Service, *gitmocks.Client) {
 	helmClient.On("ExtractChart", chart, version).Return("./testdata/my-chart", io.NopCloser, nil)
 	helmClient.On("CleanChartCache", chart, version).Return(nil)
 
-	service.newGitClient = func(rawRepoURL string, creds git.Creds, insecure bool, enableLfs bool) (client git.Client, e error) {
+	service.newGitClient = func(rawRepoURL string, creds git.Creds, insecure bool, enableLfs bool, opts ...git.ClientOpts) (client git.Client, e error) {
 		return gitClient, nil
 	}
-	service.newHelmClient = func(repoURL string, creds helm.Creds, enableOci bool) helm.Client {
+	service.newHelmClient = func(repoURL string, creds helm.Creds, enableOci bool, opts ...helm.ClientOpts) helm.Client {
 		return helmClient
 	}
 	return service, gitClient
@@ -115,7 +116,7 @@ func newServiceWithCommitSHA(root, revision string) *Service {
 		gitClient.On("Root").Return(root)
 	})
 
-	service.newGitClient = func(rawRepoURL string, creds git.Creds, insecure bool, enableLfs bool) (client git.Client, e error) {
+	service.newGitClient = func(rawRepoURL string, creds git.Creds, insecure bool, enableLfs bool, opts ...git.ClientOpts) (client git.Client, e error) {
 		return gitClient, nil
 	}
 
diff --git a/server/server_test.go b/server/server_test.go
index fa762a473..f5764005a 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -225,7 +225,7 @@ func TestInitializingExistingDefaultProject(t *testing.T) {
 	secret := test.NewFakeSecret()
 	kubeclientset := fake.NewSimpleClientset(cm, secret)
 	defaultProj := &v1alpha1.AppProject{
-		ObjectMeta: metav1.ObjectMeta{Name: common.DefaultAppProjectName, Namespace: test.FakeArgoCDNamespace},
+		ObjectMeta: metav1.ObjectMeta{Name: v1alpha1.DefaultAppProjectName, Namespace: test.FakeArgoCDNamespace},
 		Spec:       v1alpha1.AppProjectSpec{},
 	}
 	appClientSet := apps.NewSimpleClientset(defaultProj)
@@ -239,10 +239,10 @@ func TestInitializingExistingDefaultProject(t *testing.T) {
 	argocd := NewServer(context.Background(), argoCDOpts)
 	assert.NotNil(t, argocd)
 
-	proj, err := appClientSet.ArgoprojV1alpha1().AppProjects(test.FakeArgoCDNamespace).Get(context.Background(), common.DefaultAppProjectName, metav1.GetOptions{})
+	proj, err := appClientSet.ArgoprojV1alpha1().AppProjects(test.FakeArgoCDNamespace).Get(context.Background(), v1alpha1.DefaultAppProjectName, metav1.GetOptions{})
 	assert.Nil(t, err)
 	assert.NotNil(t, proj)
-	assert.Equal(t, proj.Name, common.DefaultAppProjectName)
+	assert.Equal(t, proj.Name, v1alpha1.DefaultAppProjectName)
 }
 
 func TestInitializingNotExistingDefaultProject(t *testing.T) {
@@ -260,10 +260,10 @@ func TestInitializingNotExistingDefaultProject(t *testing.T) {
 	argocd := NewServer(context.Background(), argoCDOpts)
 	assert.NotNil(t, argocd)
 
-	proj, err := appClientSet.ArgoprojV1alpha1().AppProjects(test.FakeArgoCDNamespace).Get(context.Background(), common.DefaultAppProjectName, metav1.GetOptions{})
+	proj, err := appClientSet.ArgoprojV1alpha1().AppProjects(test.FakeArgoCDNamespace).Get(context.Background(), v1alpha1.DefaultAppProjectName, metav1.GetOptions{})
 	assert.Nil(t, err)
 	assert.NotNil(t, proj)
-	assert.Equal(t, proj.Name, common.DefaultAppProjectName)
+	assert.Equal(t, proj.Name, v1alpha1.DefaultAppProjectName)
 }
 
 func TestEnforceProjectGroups(t *testing.T) {
@@ -510,7 +510,7 @@ func TestInitializeDefaultProject_ProjectDoesNotExist(t *testing.T) {
 	}
 
 	proj, err := argoCDOpts.AppClientset.ArgoprojV1alpha1().
-		AppProjects(test.FakeArgoCDNamespace).Get(context.Background(), common.DefaultAppProjectName, metav1.GetOptions{})
+		AppProjects(test.FakeArgoCDNamespace).Get(context.Background(), v1alpha1.DefaultAppProjectName, metav1.GetOptions{})
 
 	if !assert.NoError(t, err) {
 		return
@@ -526,7 +526,7 @@ func TestInitializeDefaultProject_ProjectDoesNotExist(t *testing.T) {
 func TestInitializeDefaultProject_ProjectAlreadyInitialized(t *testing.T) {
 	existingDefaultProject := v1alpha1.AppProject{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:      common.DefaultAppProjectName,
+			Name:      v1alpha1.DefaultAppProjectName,
 			Namespace: test.FakeArgoCDNamespace,
 		},
 		Spec: v1alpha1.AppProjectSpec{
@@ -547,7 +547,7 @@ func TestInitializeDefaultProject_ProjectAlreadyInitialized(t *testing.T) {
 	}
 
 	proj, err := argoCDOpts.AppClientset.ArgoprojV1alpha1().
-		AppProjects(test.FakeArgoCDNamespace).Get(context.Background(), common.DefaultAppProjectName, metav1.GetOptions{})
+		AppProjects(test.FakeArgoCDNamespace).Get(context.Background(), v1alpha1.DefaultAppProjectName, metav1.GetOptions{})
 
 	if !assert.NoError(t, err) {
 		return
diff --git a/test/e2e/app_management_test.go b/test/e2e/app_management_test.go
index 1aaa39f97..ad06558c5 100644
--- a/test/e2e/app_management_test.go
+++ b/test/e2e/app_management_test.go
@@ -111,7 +111,7 @@ func TestAppCreation(t *testing.T) {
 			assert.Equal(t, RepoURL(RepoURLTypeFile), app.Spec.Source.RepoURL)
 			assert.Equal(t, guestbookPath, app.Spec.Source.Path)
 			assert.Equal(t, DeploymentNamespace(), app.Spec.Destination.Namespace)
-			assert.Equal(t, common.KubernetesInternalAPIServerAddr, app.Spec.Destination.Server)
+			assert.Equal(t, KubernetesInternalAPIServerAddr, app.Spec.Destination.Server)
 		}).
 		Expect(Event(EventReasonResourceCreated, "create")).
 		And(func(_ *Application) {
@@ -807,10 +807,10 @@ func TestPermissions(t *testing.T) {
 
 	// make sure app cannot be created without permissions in project
 	_, err = RunCli("app", "create", appName, "--repo", RepoURL(RepoURLTypeFile),
-		"--path", guestbookPath, "--project", "test", "--dest-server", common.KubernetesInternalAPIServerAddr, "--dest-namespace", DeploymentNamespace())
+		"--path", guestbookPath, "--project", "test", "--dest-server", KubernetesInternalAPIServerAddr, "--dest-namespace", DeploymentNamespace())
 	assert.Error(t, err)
 	sourceError := fmt.Sprintf("application repo %s is not permitted in project 'test'", RepoURL(RepoURLTypeFile))
-	destinationError := fmt.Sprintf("application destination {%s %s} is not permitted in project 'test'", common.KubernetesInternalAPIServerAddr, DeploymentNamespace())
+	destinationError := fmt.Sprintf("application destination {%s %s} is not permitted in project 'test'", KubernetesInternalAPIServerAddr, DeploymentNamespace())
 
 	assert.Contains(t, err.Error(), sourceError)
 	assert.Contains(t, err.Error(), destinationError)
@@ -825,7 +825,7 @@ func TestPermissions(t *testing.T) {
 
 	// make sure controller report permissions issues in conditions
 	_, err = RunCli("app", "create", appName, "--repo", RepoURL(RepoURLTypeFile),
-		"--path", guestbookPath, "--project", "test", "--dest-server", common.KubernetesInternalAPIServerAddr, "--dest-namespace", DeploymentNamespace())
+		"--path", guestbookPath, "--project", "test", "--dest-server", KubernetesInternalAPIServerAddr, "--dest-namespace", DeploymentNamespace())
 	assert.NoError(t, err)
 	defer func() {
 		err = AppClientset.ArgoprojV1alpha1().Applications(ArgoCDNamespace).Delete(context.Background(), appName, metav1.DeleteOptions{})
@@ -1398,7 +1398,7 @@ func TestCreateDisableValidation(t *testing.T) {
 		Then().
 		And(func(app *Application) {
 			_, err := RunCli("app", "create", app.Name, "--upsert", "--validate=false", "--repo", RepoURL(RepoURLTypeFile),
-				"--path", "baddir2", "--project", app.Spec.Project, "--dest-server", common.KubernetesInternalAPIServerAddr, "--dest-namespace", DeploymentNamespace())
+				"--path", "baddir2", "--project", app.Spec.Project, "--dest-server", KubernetesInternalAPIServerAddr, "--dest-namespace", DeploymentNamespace())
 			assert.NoError(t, err)
 		}).
 		When().
@@ -1598,3 +1598,34 @@ func TestSyncOptionReplace(t *testing.T) {
 			assert.Equal(t, app.Status.OperationState.SyncResult.Resources[0].Message, "configmap/my-map replaced")
 		})
 }
+
+func TestDiscoverNewCommit(t *testing.T) {
+	var sha string
+	Given(t).
+		Path("config-map").
+		When().
+		Create().
+		Sync().
+		Then().
+		Expect(SyncStatusIs(SyncStatusCodeSynced)).
+		And(func(app *Application) {
+			sha = app.Status.Sync.Revision
+			assert.NotEmpty(t, sha)
+		}).
+		When().
+		PatchFile("config-map.yaml", `[{"op": "replace", "path": "/data/foo", "value": "hello"}]`).
+		Then().
+		Expect(SyncStatusIs(SyncStatusCodeSynced)).
+		// make sure new commit is not discovered immediately after push
+		And(func(app *Application) {
+			assert.Equal(t, sha, app.Status.Sync.Revision)
+		}).
+		When().
+		// make sure new commit is not discovered after refresh is requested
+		Refresh(RefreshTypeNormal).
+		Then().
+		Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
+		And(func(app *Application) {
+			assert.NotEqual(t, sha, app.Status.Sync.Revision)
+		})
+}
diff --git a/test/e2e/fixture/app/context.go b/test/e2e/fixture/app/context.go
index 7ee01f1bc..c2db065f3 100644
--- a/test/e2e/fixture/app/context.go
+++ b/test/e2e/fixture/app/context.go
@@ -4,7 +4,6 @@ import (
 	"testing"
 	"time"
 
-	. "github.com/argoproj/argo-cd/v2/common"
 	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
 	"github.com/argoproj/argo-cd/v2/test/e2e/fixture"
 	"github.com/argoproj/argo-cd/v2/test/e2e/fixture/certs"
@@ -44,7 +43,7 @@ func Given(t *testing.T) *Context {
 	// ARGOCE_E2E_DEFAULT_TIMEOUT can be used to override the default timeout
 	// for any context.
 	timeout := env.ParseNumFromEnv("ARGOCD_E2E_DEFAULT_TIMEOUT", 10, 0, 180)
-	return &Context{t: t, destServer: KubernetesInternalAPIServerAddr, repoURLType: fixture.RepoURLTypeFile, name: fixture.Name(), timeout: timeout, project: "default", prune: true}
+	return &Context{t: t, destServer: v1alpha1.KubernetesInternalAPIServerAddr, repoURLType: fixture.RepoURLTypeFile, name: fixture.Name(), timeout: timeout, project: "default", prune: true}
 }
 
 func (c *Context) GPGPublicKeyAdded() *Context {
diff --git a/test/e2e/project_management_test.go b/test/e2e/project_management_test.go
index 637ff3fdb..eb11bdfb6 100644
--- a/test/e2e/project_management_test.go
+++ b/test/e2e/project_management_test.go
@@ -15,7 +15,6 @@ import (
 	"k8s.io/apimachinery/pkg/fields"
 	"k8s.io/utils/pointer"
 
-	"github.com/argoproj/argo-cd/v2/common"
 	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
 	"github.com/argoproj/argo-cd/v2/test/e2e/fixture"
 	"github.com/argoproj/argo-cd/v2/util/argo"
@@ -290,7 +289,7 @@ func TestUseJWTToken(t *testing.T) {
 				Path:    "guestbook",
 			},
 			Destination: v1alpha1.ApplicationDestination{
-				Server:    common.KubernetesInternalAPIServerAddr,
+				Server:    v1alpha1.KubernetesInternalAPIServerAddr,
 				Namespace: fixture.ArgoCDNamespace,
 			},
 			Project: projectName,
@@ -300,7 +299,7 @@ func TestUseJWTToken(t *testing.T) {
 		ObjectMeta: metav1.ObjectMeta{Name: projectName},
 		Spec: v1alpha1.AppProjectSpec{
 			Destinations: []v1alpha1.ApplicationDestination{{
-				Server:    common.KubernetesInternalAPIServerAddr,
+				Server:    v1alpha1.KubernetesInternalAPIServerAddr,
 				Namespace: fixture.ArgoCDNamespace,
 			}},
 			SourceRepos: []string{"*"},
@@ -507,7 +506,7 @@ func TestGetVirtualProjectNoMatch(t *testing.T) {
 	projectName := "proj-" + fixture.Name()
 	_, err = fixture.RunCli("proj", "create", projectName,
 		"--description", "Test description",
-		"-d", fmt.Sprintf("%s,*", common.KubernetesInternalAPIServerAddr),
+		"-d", fmt.Sprintf("%s,*", v1alpha1.KubernetesInternalAPIServerAddr),
 		"-s", "*",
 		"--orphaned-resources")
 	assert.NoError(t, err)
@@ -517,7 +516,7 @@ func TestGetVirtualProjectNoMatch(t *testing.T) {
 
 	//Create an app belongs to proj project
 	_, err = fixture.RunCli("app", "create", fixture.Name(), "--repo", fixture.RepoURL(fixture.RepoURLTypeFile),
-		"--path", guestbookPath, "--project", proj.Name, "--dest-server", common.KubernetesInternalAPIServerAddr, "--dest-namespace", fixture.DeploymentNamespace())
+		"--path", guestbookPath, "--project", proj.Name, "--dest-server", v1alpha1.KubernetesInternalAPIServerAddr, "--dest-namespace", fixture.DeploymentNamespace())
 	assert.NoError(t, err)
 
 	//App trying to sync a resource which is not blacked listed anywhere
@@ -539,7 +538,7 @@ func TestGetVirtualProjectMatch(t *testing.T) {
 	projectName := "proj-" + fixture.Name()
 	_, err = fixture.RunCli("proj", "create", projectName,
 		"--description", "Test description",
-		"-d", fmt.Sprintf("%s,*", common.KubernetesInternalAPIServerAddr),
+		"-d", fmt.Sprintf("%s,*", v1alpha1.KubernetesInternalAPIServerAddr),
 		"-s", "*",
 		"--orphaned-resources")
 	assert.NoError(t, err)
@@ -554,7 +553,7 @@ func TestGetVirtualProjectMatch(t *testing.T) {
 
 	//Create an app belongs to proj project
 	_, err = fixture.RunCli("app", "create", fixture.Name(), "--repo", fixture.RepoURL(fixture.RepoURLTypeFile),
-		"--path", guestbookPath, "--project", proj.Name, "--dest-server", common.KubernetesInternalAPIServerAddr, "--dest-namespace", fixture.DeploymentNamespace())
+		"--path", guestbookPath, "--project", proj.Name, "--dest-server", v1alpha1.KubernetesInternalAPIServerAddr, "--dest-namespace", fixture.DeploymentNamespace())
 	assert.NoError(t, err)
 
 	//App trying to sync a resource which is not blacked listed anywhere
diff --git a/util/db/cluster_norace_test.go b/util/db/cluster_norace_test.go
index 24043bd2a..df34f1be7 100644
--- a/util/db/cluster_norace_test.go
+++ b/util/db/cluster_norace_test.go
@@ -9,7 +9,6 @@ import (
 	"github.com/stretchr/testify/assert"
 	"k8s.io/client-go/kubernetes/fake"
 
-	"github.com/argoproj/argo-cd/v2/common"
 	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
 	"github.com/argoproj/argo-cd/v2/util/settings"
 )
@@ -26,7 +25,7 @@ func TestWatchClusters_CreateRemoveCluster(t *testing.T) {
 	runWatchTest(t, db, []func(old *v1alpha1.Cluster, new *v1alpha1.Cluster){
 		func(old *v1alpha1.Cluster, new *v1alpha1.Cluster) {
 			assert.Nil(t, old)
-			assert.Equal(t, new.Server, common.KubernetesInternalAPIServerAddr)
+			assert.Equal(t, new.Server, v1alpha1.KubernetesInternalAPIServerAddr)
 
 			_, err := db.CreateCluster(context.Background(), &v1alpha1.Cluster{
 				Server: "https://minikube",
@@ -60,23 +59,23 @@ func TestWatchClusters_LocalClusterModifications(t *testing.T) {
 	runWatchTest(t, db, []func(old *v1alpha1.Cluster, new *v1alpha1.Cluster){
 		func(old *v1alpha1.Cluster, new *v1alpha1.Cluster) {
 			assert.Nil(t, old)
-			assert.Equal(t, new.Server, common.KubernetesInternalAPIServerAddr)
+			assert.Equal(t, new.Server, v1alpha1.KubernetesInternalAPIServerAddr)
 
 			_, err := db.CreateCluster(context.Background(), &v1alpha1.Cluster{
-				Server: common.KubernetesInternalAPIServerAddr,
+				Server: v1alpha1.KubernetesInternalAPIServerAddr,
 				Name:   "some name",
 			})
 			assert.NoError(t, err)
 		},
 		func(old *v1alpha1.Cluster, new *v1alpha1.Cluster) {
 			assert.NotNil(t, old)
-			assert.Equal(t, new.Server, common.KubernetesInternalAPIServerAddr)
+			assert.Equal(t, new.Server, v1alpha1.KubernetesInternalAPIServerAddr)
 			assert.Equal(t, new.Name, "some name")
 
-			assert.NoError(t, db.DeleteCluster(context.Background(), common.KubernetesInternalAPIServerAddr))
+			assert.NoError(t, db.DeleteCluster(context.Background(), v1alpha1.KubernetesInternalAPIServerAddr))
 		},
 		func(old *v1alpha1.Cluster, new *v1alpha1.Cluster) {
-			assert.Equal(t, new.Server, common.KubernetesInternalAPIServerAddr)
+			assert.Equal(t, new.Server, v1alpha1.KubernetesInternalAPIServerAddr)
 			assert.Equal(t, new.Name, "in-cluster")
 		},
 	})
diff --git a/util/db/cluster_test.go b/util/db/cluster_test.go
index 95b0dc50c..ea43192d2 100644
--- a/util/db/cluster_test.go
+++ b/util/db/cluster_test.go
@@ -96,7 +96,7 @@ func TestUpdateCluster(t *testing.T) {
 		return
 	}
 
-	assert.Equal(t, secret.Annotations[common.AnnotationKeyRefresh], requestedAt.Format(time.RFC3339))
+	assert.Equal(t, secret.Annotations[v1alpha1.AnnotationKeyRefresh], requestedAt.Format(time.RFC3339))
 }
 
 func TestDeleteUnknownCluster(t *testing.T) {
diff --git a/util/helm/client_test.go b/util/helm/client_test.go
index 7036c1f4c..fb3f578a5 100644
--- a/util/helm/client_test.go
+++ b/util/helm/client_test.go
@@ -1,15 +1,31 @@
 package helm
 
 import (
+	"bytes"
 	"os"
 	"testing"
-	"time"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"gopkg.in/yaml.v2"
 
 	"github.com/argoproj/argo-cd/v2/util/io"
 )
 
+type fakeIndexCache struct {
+	data []byte
+}
+
+func (f *fakeIndexCache) SetHelmIndex(_ string, indexData []byte) error {
+	f.data = indexData
+	return nil
+}
+
+func (f *fakeIndexCache) GetHelmIndex(_ string, indexData *[]byte) error {
+	*indexData = f.data
+	return nil
+}
+
 func TestIndex(t *testing.T) {
 	t.Run("Invalid", func(t *testing.T) {
 		client := NewClient("", Creds{}, false)
@@ -33,16 +49,16 @@ func TestIndex(t *testing.T) {
 	})
 
 	t.Run("Cached", func(t *testing.T) {
-		var prev time.Duration
-		indexDuration, prev = time.Minute, indexDuration
-		defer func() {
-			indexDuration = prev
-		}()
+		fakeIndex := Index{Entries: map[string]Entries{"fake": {}}}
+		data := bytes.Buffer{}
+		err := yaml.NewEncoder(&data).Encode(fakeIndex)
+		require.NoError(t, err)
 
-		client := NewClient("https://argoproj.github.io/argo-helm", Creds{}, false)
+		client := NewClient("https://argoproj.github.io/argo-helm", Creds{}, false, WithIndexCache(&fakeIndexCache{data: data.Bytes()}))
 		index, err := client.GetIndex(false)
+
 		assert.NoError(t, err)
-		assert.NotNil(t, index)
+		assert.Equal(t, fakeIndex, *index)
 	})
 
 }
diff --git a/util/webhook/webhook_test.go b/util/webhook/webhook_test.go
index a16392b22..c679e28d3 100644
--- a/util/webhook/webhook_test.go
+++ b/util/webhook/webhook_test.go
@@ -12,7 +12,6 @@ import (
 	"github.com/stretchr/testify/assert"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
-	"github.com/argoproj/argo-cd/v2/common"
 	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
 	appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned/fake"
 	"github.com/argoproj/argo-cd/v2/reposerver/cache"
@@ -32,6 +31,7 @@ func NewMockHandler() *ArgoCDWebhookHandler {
 	return NewHandler("", appClientset, &settings.ArgoCDSettings{}, &fakeSettingsSrc{}, cache.NewCache(
 		cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Hour)),
 		1*time.Minute,
+		1*time.Minute,
 	))
 }
 
@@ -175,7 +175,7 @@ func getApp(annotation string, sourcePath string) *v1alpha1.Application {
 	return &v1alpha1.Application{
 		ObjectMeta: metav1.ObjectMeta{
 			Annotations: map[string]string{
-				common.AnnotationKeyManifestGeneratePaths: annotation,
+				v1alpha1.AnnotationKeyManifestGeneratePaths: annotation,
 			},
 		},
 		Spec: v1alpha1.ApplicationSpec{