diff --git a/app/assets/javascripts/repository/components/commit_info.vue b/app/assets/javascripts/repository/components/commit_info.vue
index f9fb971b2968fcc056cc727071bbd692a1106f2e..64578570224ab9ab80e7312c1531ce7af985983d 100644
--- a/app/assets/javascripts/repository/components/commit_info.vue
+++ b/app/assets/javascripts/repository/components/commit_info.vue
@@ -146,13 +146,37 @@ export default {
+
- {{ commit.committerName }}
+
+ {{ commit.committer.name }}
+
+
+ {{ commit.committerName }}
+
+
+ {{ commit.committerName }}
+
diff --git a/app/graphql/queries/repository/path_last_commit.query.graphql b/app/graphql/queries/repository/path_last_commit.query.graphql
index d90ce5c16693a44ff904fe1a3fe2bb325d6ea956..d69b15f212508f6474cc44509ebead6f650d1318 100644
--- a/app/graphql/queries/repository/path_last_commit.query.graphql
+++ b/app/graphql/queries/repository/path_last_commit.query.graphql
@@ -15,6 +15,8 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!, $refType:
webPath
committerName
committerEmail
+ committerAvatarUrl
+ committerWebUrl
committedDate
authoredDate
authorName
@@ -27,6 +29,13 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!, $refType:
avatarUrl
webPath
}
+ committer {
+ __typename
+ id
+ name
+ avatarUrl
+ webPath
+ }
signature {
__typename
... on GpgSignature {
diff --git a/app/graphql/types/repositories/commit_type.rb b/app/graphql/types/repositories/commit_type.rb
index 1c6a6d22162cb2f81112ed27b87946674a3a45b3..e822dcaac88fa7a47c62d5633c1b9c76aa4a6bc4 100644
--- a/app/graphql/types/repositories/commit_type.rb
+++ b/app/graphql/types/repositories/commit_type.rb
@@ -65,10 +65,19 @@ class CommitType < BaseObject
field :committer_name, type: GraphQL::Types::String, null: true,
description: "Name of the committer."
+ field :committer_avatar_url, type: GraphQL::Types::String, null: true,
+ description: 'Avatar URL of the committer.'
+
+ field :committer_web_url, type: GraphQL::Types::String, null: true,
+ description: 'Web URL of the committer.'
+
# models/commit lazy loads the author by email
field :author, type: Types::UserType, null: true,
description: 'Author of the commit.'
+ field :committer, type: Types::UserType, null: true,
+ description: 'Committer of the commit.'
+
field :diffs, [Types::DiffType], null: true, calls_gitaly: true,
description: 'Diffs contained within the commit. ' \
'This field can only be resolved for 10 diffs in any single request.' do
@@ -93,6 +102,20 @@ def diffs
def author_gravatar
GravatarService.new.execute(object.author_email, 40)
end
+
+ def committer_avatar_url
+ if object.committer
+ object.committer.avatar_url
+ elsif object.committer_email
+ GravatarService.new.execute(object.committer_email, 40)
+ end
+ end
+
+ def committer_web_url
+ return unless object.committer
+
+ Gitlab::Routing.url_helpers.user_url(object.committer)
+ end
end
end
end
diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md
index 4ede04ffbf3b2458512b61e719c6c3eb0a92ca08..cc851156eea454e52f1300ebfd671f7495918d7b 100644
--- a/doc/api/graphql/reference/_index.md
+++ b/doc/api/graphql/reference/_index.md
@@ -26653,8 +26653,11 @@ Represents a summary of the compared codequality report.
| `authorName` | [`String`](#string) | Commit authors name. |
| `authoredDate` | [`Time`](#time) | Timestamp of when the commit was authored. |
| `committedDate` | [`Time`](#time) | Timestamp of when the commit was committed. |
+| `committer` | [`UserCore`](#usercore) | Committer of the commit. |
+| `committerAvatarUrl` | [`String`](#string) | Avatar URL of the committer. |
| `committerEmail` | [`String`](#string) | Email of the committer. |
| `committerName` | [`String`](#string) | Name of the committer. |
+| `committerWebUrl` | [`String`](#string) | Web URL of the committer. |
| `description` | [`String`](#string) | Description of the commit message. |
| `descriptionHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `description`. |
| `diffs` | [`[Diff!]`](#diff) | Diffs contained within the commit. This field can only be resolved for 10 diffs in any single request. |
diff --git a/spec/frontend/repository/components/commit_info_spec.js b/spec/frontend/repository/components/commit_info_spec.js
index 1a451030f65ca5e1e71df06f6bbf8b5145818387..0f5fb368353a7ca1bfdefe4cf9a48e47a38b0bd2 100644
--- a/spec/frontend/repository/components/commit_info_spec.js
+++ b/spec/frontend/repository/components/commit_info_spec.js
@@ -160,5 +160,134 @@ describe('Repository last commit component', () => {
commitMockWithDifferentCommitter.committedDate,
);
});
+
+ describe('when committer is a GitLab user', () => {
+ const commitMockWithCommitterUser = {
+ author: { name: 'John Doe', email: 'john@example.com' },
+ committerName: 'Jane Smith',
+ committerEmail: 'jane@example.com',
+ committedDate: '2019-02-01',
+ committer: {
+ name: 'Jane Smith',
+ avatarUrl: 'https://gitlab.com/jane-avatar.jpg',
+ webPath: '/jane-smith',
+ },
+ };
+
+ beforeEach(() => createComponent({ commitMock: commitMockWithCommitterUser }));
+
+ it('displays clickable committer avatar using UserAvatarLink', () => {
+ const avatarLinks = wrapper.findAllComponents(UserAvatarLink);
+ const committerAvatar = avatarLinks.at(1); // First is author, second is committer
+
+ expect(committerAvatar.exists()).toBe(true);
+ expect(committerAvatar.props()).toMatchObject({
+ linkHref: '/jane-smith',
+ imgSrc: 'https://gitlab.com/jane-avatar.jpg',
+ imgAlt: 'Jane Smith',
+ imgSize: 16,
+ });
+ });
+
+ it('displays committer name as a clickable link', () => {
+ const committerLink = wrapper.find('.commit-committer-link');
+
+ expect(committerLink.exists()).toBe(true);
+ expect(committerLink.attributes('href')).toBe(
+ commitMockWithCommitterUser.committer.webPath,
+ );
+ expect(committerLink.text()).toBe('Jane Smith');
+ });
+ });
+
+ describe('when committer is not a GitLab user but has avatar URL', () => {
+ const commitMockWithNonUserCommitter = {
+ author: { name: 'John Doe', email: 'john@example.com' },
+ committerName: 'Jane Smith',
+ committerEmail: 'jane@example.com',
+ committerAvatarUrl: 'https://gravatar.com/avatar/123',
+ committerWebUrl: null,
+ committedDate: '2019-02-01',
+ committer: null,
+ };
+
+ beforeEach(() => createComponent({ commitMock: commitMockWithNonUserCommitter }));
+
+ it('displays committer avatar without link using UserAvatarImage', () => {
+ const avatarImages = findUserAvatarImages();
+ const committerAvatar = avatarImages.at(0);
+
+ expect(committerAvatar.exists()).toBe(true);
+ expect(committerAvatar.props()).toMatchObject({
+ size: 16,
+ imgSrc: 'https://gravatar.com/avatar/123',
+ });
+ });
+
+ it('displays committer name as plain text without link', () => {
+ const committerLink = wrapper.find('.commit-committer-link');
+ const text = findCommitterWrapper().text();
+
+ expect(committerLink.exists()).toBe(false);
+ expect(text).toContain('Jane Smith');
+ });
+ });
+
+ describe('when committer has web URL but no user object', () => {
+ const commitMockWithWebUrl = {
+ author: { name: 'John Doe', email: 'john@example.com' },
+ committerName: 'Jane Smith',
+ committerEmail: 'jane@example.com',
+ committerAvatarUrl: 'https://gravatar.com/avatar/123',
+ committerWebUrl: '/jane-smith',
+ committedDate: '2019-02-01',
+ committer: null,
+ };
+
+ beforeEach(() => createComponent({ commitMock: commitMockWithWebUrl }));
+
+ it('displays committer avatar without UserAvatarLink', () => {
+ const avatarImages = findUserAvatarImages();
+
+ expect(avatarImages.at(0).exists()).toBe(true);
+ expect(avatarImages.at(0).props('imgSrc')).toBe('https://gravatar.com/avatar/123');
+ });
+
+ it('displays committer name as a clickable link', () => {
+ const committerLink = wrapper.find('.commit-committer-link');
+
+ expect(committerLink.exists()).toBe(true);
+ expect(committerLink.attributes('href')).toBe('/jane-smith');
+ expect(committerLink.text()).toBe('Jane Smith');
+ });
+ });
+
+ describe('when committer has no avatar or profile URL', () => {
+ const commitMockMinimalCommitter = {
+ author: { name: 'John Doe', email: 'john@example.com' },
+ committerName: 'Jane Smith',
+ committerEmail: 'jane@example.com',
+ committerAvatarUrl: null,
+ committerWebUrl: null,
+ committedDate: '2019-02-01',
+ committer: null,
+ };
+
+ beforeEach(() => createComponent({ commitMock: commitMockMinimalCommitter }));
+
+ it('does not display committer avatar', () => {
+ const avatarImages = findUserAvatarImages();
+
+ expect(avatarImages).toHaveLength(0);
+ });
+
+ it('displays committer name as plain text', () => {
+ const committerLink = wrapper.find('.commit-committer-link');
+ const text = findCommitterWrapper().text();
+
+ expect(committerLink.exists()).toBe(false);
+ expect(text).toContain('Jane Smith');
+ });
+ });
});
});
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index dc181d86b831dadc1d103b78af5e8531c45a7ff4..a05d72bfcbd5d9c714b80aaf146791ca61b12ee8 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -70,7 +70,7 @@ describe('Repository last commit component', () => {
await waitForPromises();
- const commit = { ...commitData.project?.repository.paginatedTree.nodes[0].lastCommit };
+ const commit = { ...commitData.data.project?.repository.lastCommit };
expect(findCommitInfo().props().commit).toMatchObject(commit);
});
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
index 23c730d8c75789a9f60362b0e53eca766d78e65b..de534f955433370cad23817a612222c65fcca002 100644
--- a/spec/frontend/repository/mock_data.js
+++ b/spec/frontend/repository/mock_data.js
@@ -340,6 +340,8 @@ export const createCommitData = ({ pipelineEdges = defaultPipelineEdges, signatu
webPath: '/commit/123',
committerName: 'Test Committer',
committerEmail: 'testcommitter@example.com',
+ committerAvatarUrl: 'https://test.com/committer-avatar',
+ committerWebUrl: 'https://test.com/committer',
committedDate: '2019-02-02',
authoredDate: '2019-01-01',
authorName: 'Test',
@@ -352,6 +354,13 @@ export const createCommitData = ({ pipelineEdges = defaultPipelineEdges, signatu
avatarUrl: 'https://test.com',
webPath: '/test',
},
+ committer: {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/2',
+ name: 'Test Committer',
+ avatarUrl: 'https://test.com/committer-avatar',
+ webPath: '/committer',
+ },
signature,
pipelines: {
__typename: 'PipelineConnection',
diff --git a/spec/graphql/types/repositories/commit_type_spec.rb b/spec/graphql/types/repositories/commit_type_spec.rb
index bc6da8931d3c67c85da740052bc44a387e16fb1d..d96cf719b6b7334c76ea1c8836894102202c5602 100644
--- a/spec/graphql/types/repositories/commit_type_spec.rb
+++ b/spec/graphql/types/repositories/commit_type_spec.rb
@@ -14,7 +14,8 @@
:id, :sha, :short_id, :title, :full_title, :full_title_html, :description, :description_html, :message,
:title_html, :authored_date,
:author_name, :author_email, :author_gravatar, :author, :diffs, :web_url, :web_path,
- :pipelines, :signature_html, :signature, :committer_name, :committer_email, :committed_date,
+ :pipelines, :signature_html, :signature, :committer_name, :committer_email, :committer_avatar_url,
+ :committer_web_url, :committer, :committed_date,
:name
)
end