diff --git a/services/gitlab/gitlab-last-commit.service.js b/services/gitlab/gitlab-last-commit.service.js
new file mode 100644
index 0000000000000000000000000000000000000000..b2342d669b0fe20932142e5c5d819862fba61c26
--- /dev/null
+++ b/services/gitlab/gitlab-last-commit.service.js
@@ -0,0 +1,79 @@
+import Joi from 'joi'
+import { optionalUrl } from '../validators.js'
+import { formatDate } from '../text-formatters.js'
+import { age as ageColor } from '../color-formatters.js'
+import { documentation, errorMessagesFor } from './gitlab-helper.js'
+import GitLabBase from './gitlab-base.js'
+
+const schema = Joi.array()
+  .items(
+    Joi.object({
+      committed_date: Joi.string().required(),
+    }).required()
+  )
+  .required()
+  .min(1)
+
+const queryParamSchema = Joi.object({
+  ref: Joi.string(),
+  gitlab_url: optionalUrl,
+}).required()
+
+const refText = `
+<p>
+  ref can be filled with the name of a branch, tag or revision range of the repository.
+</p>
+`
+
+const defaultDocumentation = documentation + refText
+
+export default class GitlabLastCommit extends GitLabBase {
+  static category = 'activity'
+
+  static route = {
+    base: 'gitlab/last-commit',
+    pattern: ':project+',
+    queryParamSchema,
+  }
+
+  static examples = [
+    {
+      title: 'GitLab last commit',
+      namedParams: {
+        project: 'gitlab-org/gitlab',
+      },
+      queryParams: { gitlab_url: 'https://gitlab.com' },
+      staticPreview: this.render({ commitDate: '2013-07-31T20:01:41Z' }),
+      documentation: defaultDocumentation,
+    },
+  ]
+
+  static defaultBadgeData = { label: 'last commit' }
+
+  static render({ commitDate }) {
+    return {
+      message: formatDate(commitDate),
+      color: ageColor(Date.parse(commitDate)),
+    }
+  }
+
+  async fetch({ project, baseUrl, ref }) {
+    // https://docs.gitlab.com/ee/api/commits.html#list-repository-commits
+    return super.fetch({
+      url: `${baseUrl}/api/v4/projects/${encodeURIComponent(
+        project
+      )}/repository/commits`,
+      options: { searchParams: { ref_name: ref } },
+      schema,
+      errorMessages: errorMessagesFor('project not found'),
+    })
+  }
+
+  async handle(
+    { project },
+    { gitlab_url: baseUrl = 'https://gitlab.com', ref }
+  ) {
+    const data = await this.fetch({ project, baseUrl, ref })
+    return this.constructor.render({ commitDate: data[0].committed_date })
+  }
+}
diff --git a/services/gitlab/gitlab-last-commit.tester.js b/services/gitlab/gitlab-last-commit.tester.js
new file mode 100644
index 0000000000000000000000000000000000000000..ecc29fdbd720892491171146c8c24a5bb16bb490
--- /dev/null
+++ b/services/gitlab/gitlab-last-commit.tester.js
@@ -0,0 +1,30 @@
+import { isFormattedDate } from '../test-validators.js'
+import { createServiceTester } from '../tester.js'
+
+export const t = await createServiceTester()
+
+t.create('last commit (recent)').get('/gitlab-org/gitlab.json').expectBadge({
+  label: 'last commit',
+  message: isFormattedDate,
+})
+
+t.create('last commit (on ref and ancient)')
+  .get('/gitlab-org/gitlab.json?ref=v13.8.6-ee')
+  .expectBadge({
+    label: 'last commit',
+    message: 'march 2021',
+  })
+
+t.create('last commit (self-managed)')
+  .get('/gitlab-cn/gitlab.json?gitlab_url=https://jihulab.com')
+  .expectBadge({
+    label: 'last commit',
+    message: isFormattedDate,
+  })
+
+t.create('last commit (project not found)')
+  .get('/open/guoxudong.io/shields-test/do-not-exist.json')
+  .expectBadge({
+    label: 'last commit',
+    message: 'project not found',
+  })