diff --git a/Gemfile b/Gemfile
index 1901455af077069ed38379e720fb3ed257b15b99..5a3a6791b298d77946c51f3c941d79818e56ec59 100644
--- a/Gemfile
+++ b/Gemfile
@@ -446,7 +446,7 @@ group :ed25519 do
 end
 
 # Gitaly GRPC protocol definitions
-gem 'gitaly', '~> 1.58.0'
+gem 'gitaly', '~> 1.65.0'
 
 gem 'grpc', '~> 1.19.0'
 
diff --git a/Gemfile.lock b/Gemfile.lock
index 7b450c262dab39e7573b120606b06179158eeee7..3142ba094cdc6d7f886abb3eed90f07a29e4fdd1 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -358,7 +358,7 @@ GEM
       po_to_json (>= 1.0.0)
       rails (>= 3.2.0)
     git (1.5.0)
-    gitaly (1.58.0)
+    gitaly (1.65.0)
       grpc (~> 1.0)
     github-markup (1.7.0)
     gitlab-labkit (0.5.2)
@@ -1168,7 +1168,7 @@ DEPENDENCIES
   gettext (~> 3.2.2)
   gettext_i18n_rails (~> 1.8.0)
   gettext_i18n_rails_js (~> 1.3)
-  gitaly (~> 1.58.0)
+  gitaly (~> 1.65.0)
   github-markup (~> 1.7.0)
   gitlab-labkit (~> 0.5)
   gitlab-license (~> 1.0)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index f084a314392f24c5c6751cf347be2d08dcbbe0f1..96b1b55e2b18e3b3025d387e3aba614ec9e82ab6 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -133,18 +133,28 @@ def commits_by(oids:)
     end
   end
 
-  def commits(ref = nil, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil, all: nil)
+  # the opts are:
+  #  - :path
+  #  - :limit
+  #  - :offset
+  #  - :skip_merges
+  #  - :after
+  #  - :before
+  #  - :all
+  #  - :first_parent
+  def commits(ref = nil, opts = {})
     options = {
       repo: raw_repository,
       ref: ref,
-      path: path,
-      limit: limit,
-      offset: offset,
-      after: after,
-      before: before,
-      follow: Array(path).length == 1,
-      skip_merges: skip_merges,
-      all: all
+      path: opts[:path],
+      follow: Array(opts[:path]).length == 1,
+      limit: opts[:limit],
+      offset: opts[:offset],
+      skip_merges: !!opts[:skip_merges],
+      after: opts[:after],
+      before: opts[:before],
+      all: !!opts[:all],
+      first_parent: !!opts[:first_parent]
     }
 
     commits = Gitlab::Git::Commit.where(options)
diff --git a/changelogs/unreleased/add-first-parent-to-find-commits.yml b/changelogs/unreleased/add-first-parent-to-find-commits.yml
new file mode 100644
index 0000000000000000000000000000000000000000..076eed90f68cac18952baf3c72bff7fe28b99a30
--- /dev/null
+++ b/changelogs/unreleased/add-first-parent-to-find-commits.yml
@@ -0,0 +1,5 @@
+---
+title: Add first_parent option to list commits api
+merge_request: 32410
+author: jhenkens
+type: added
diff --git a/doc/api/commits.md b/doc/api/commits.md
index b41409b4b927b463768a07302e6d70157e7ddb24..3927a4bbc6207b8066cf6fe7ee27fba13c99cf51 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -11,12 +11,13 @@ GET /projects/:id/repository/commits
 | Attribute | Type | Required | Description |
 | --------- | ---- | -------- | ----------- |
 | `id`      | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
-| `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch |
+| `ref_name` | string | no | The name of a repository branch, tag or revision range, or if not given the default branch |
 | `since` | string | no | Only commits after or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
 | `until` | string | no | Only commits before or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
 | `path` | string | no | The file path |
 | `all` | boolean | no | Retrieve every commit from the repository |
 | `with_stats` | boolean | no | Stats about each commit will be added to the response |
+| `first_parent` | boolean | no | Follow only the first parent commit upon seeing a merge commit |
 
 ```bash
 curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/commits"
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index a2f3e87ebd26b725401113591e2a060cc985928e..ffff40141de5e7b34ae92537564aeb0fe426e95d 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -37,6 +37,7 @@ def authorize_push_to_branch!(branch)
         optional :path, type: String, desc: 'The file path'
         optional :all, type: Boolean, desc: 'Every commit will be returned'
         optional :with_stats, type: Boolean, desc: 'Stats about each commit will be added to the response'
+        optional :first_parent, type: Boolean, desc: 'Only include the first parent of merges'
         use :pagination
       end
       get ':id/repository/commits' do
@@ -47,6 +48,7 @@ def authorize_push_to_branch!(branch)
         offset = (params[:page] - 1) * params[:per_page]
         all = params[:all]
         with_stats = params[:with_stats]
+        first_parent = params[:first_parent]
 
         commits = user_project.repository.commits(ref,
                                                   path: path,
@@ -54,11 +56,12 @@ def authorize_push_to_branch!(branch)
                                                   offset: offset,
                                                   before: before,
                                                   after: after,
-                                                  all: all)
+                                                  all: all,
+                                                  first_parent: first_parent)
 
         commit_count =
-          if all || path || before || after
-            user_project.repository.count_commits(ref: ref, path: path, before: before, after: after, all: all)
+          if all || path || before || after || first_parent
+            user_project.repository.count_commits(ref: ref, path: path, before: before, after: after, all: all, first_parent: first_parent)
           else
             # Cacheable commit count.
             user_project.repository.commit_count_for_ref(ref)
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index a80ce462ab00c3d900393909f90f46310e217047..5c79a9c5a259787e91f5d892c99287e1437dbcaa 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -140,7 +140,8 @@ def commit_count(ref, options = {})
         request = Gitaly::CountCommitsRequest.new(
           repository: @gitaly_repo,
           revision: encode_binary(ref),
-          all: !!options[:all]
+          all: !!options[:all],
+          first_parent: !!options[:first_parent]
         )
         request.after = Google::Protobuf::Timestamp.new(seconds: options[:after].to_i) if options[:after].present?
         request.before = Google::Protobuf::Timestamp.new(seconds: options[:before].to_i) if options[:before].present?
@@ -325,6 +326,7 @@ def find_commits(options)
           follow:       options[:follow],
           skip_merges:  options[:skip_merges],
           all:          !!options[:all],
+          first_parent: !!options[:first_parent],
           disable_walk: true # This option is deprecated. The 'walk' implementation is being removed.
         )
         request.after    = GitalyClient.timestamp(options[:after]) if options[:after]
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 6dc47e0e5019ce40c38b5fb350100563e1c78888..011b46c7f1af1c0fad3055de144779cfbd308ca2 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -279,7 +279,7 @@ def create_commit_with_invalid_utf8_path
   describe '#commits' do
     context 'when neither the all flag nor a ref are specified' do
       it 'returns every commit from default branch' do
-        expect(repository.commits(limit: 60).size).to eq(37)
+        expect(repository.commits(nil, limit: 60).size).to eq(37)
       end
     end
 
@@ -320,7 +320,7 @@ def create_commit_with_invalid_utf8_path
 
     context "when 'all' flag is set" do
       it 'returns every commit from the repository' do
-        expect(repository.commits(all: true, limit: 60).size).to eq(60)
+        expect(repository.commits(nil, all: true, limit: 60).size).to eq(60)
       end
     end
   end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 5e6ff40e8cf2f13e36417f91b1c46f9e76120664..90ff1d12bf14e88520bac45581355325366eaf5f 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -169,6 +169,18 @@
         end
       end
 
+      context 'first_parent optional parameter' do
+        it 'returns all first_parent commits' do
+          commit_count = project.repository.count_commits(ref: SeedRepo::Commit::ID, first_parent: true)
+
+          get api("/projects/#{project_id}/repository/commits", user), params: { ref_name: SeedRepo::Commit::ID, first_parent: 'true' }
+
+          expect(response).to include_pagination_headers
+          expect(commit_count).to eq(12)
+          expect(response.headers['X-Total']).to eq(commit_count.to_s)
+        end
+      end
+
       context 'with_stats optional parameter' do
         let(:project) { create(:project, :public, :repository) }