DEV Community

Amir Dhaouadi
Amir Dhaouadi

Posted on

GitLab: Require Squash Commits for Pull Requests to the “develop” Branch

Context

On GitLab, squash enforcement has to go through CI, because the native setting applies to the whole project. In Settings → Merge requests → Squash commits when merging, the Require option would also squash the develop → master release MRs, which must keep their history.

GitLab does have an advantage over GitHub: every MR carries a "Squash commits when merge request is accepted" checkbox, readable in CI through CI_MERGE_REQUEST_SQUASH_ON_MERGE and editable through the API. The three options below rely on jobs that only run for MRs into develop.

Shared prerequisite: enable Pipelines must succeed (Settings → Merge requests). Without it, a failed job shows a red cross but does not prevent the merge. Squashing must also be allowed (not set to "Do not allow").

Option 1 — Block when the squash box is unticked

The job reads the checkbox when the pipeline starts and fails if it is not ticked. The pipeline turns red, so the Merge button stays greyed out.

squash-required:
  stage: .pre
  image: public.ecr.aws/docker/library/alpine:3.20
  script:
    - |
      if [ "$CI_MERGE_REQUEST_SQUASH_ON_MERGE" != "true" ]; then
        echo "Merge requests into develop must be squashed: tick 'Squash commits when merge request is accepted', then run a new pipeline."
        exit 1
      fi
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop"
Enter fullscreen mode Exit fullscreen mode
  • stage: .pre runs the job before all others, so the failure shows up immediately.
  • To unblock, the developer ticks the box and starts a new pipeline. Simply retrying the job would reuse the old value of the variable.
  • Limitation: the checkbox is only read at startup. Unticking it after a green pipeline allows a merge without squash.

Option 2 — Tick the box automatically through the API

Instead of failing, the job ticks the box on the MR with a call to the GitLab API. GitLab reads the option at merge time, so the squash will be applied. Without a token, or if the call fails, the job falls back to the blocking behaviour of option 1.

Setup:

  1. Create a project access token (Settings → Access tokens), role Developer, scope api, with a short expiry.
  2. Add it under Settings → CI/CD → Variables as SQUASH_BOT_TOKEN, Masked but not Protected, otherwise feature branches do not receive it.
  3. Replace the job with this one:
squash-required:
  stage: .pre
  image: public.ecr.aws/docker/library/alpine:3.20
  script:
    - |
      if [ "$CI_MERGE_REQUEST_SQUASH_ON_MERGE" = "true" ]; then
        exit 0
      fi
      if [ -n "$SQUASH_BOT_TOKEN" ] \
        && apk add --no-cache curl > /dev/null \
        && curl --fail -sS -X PUT \
             --header "PRIVATE-TOKEN: $SQUASH_BOT_TOKEN" \
             --data "squash=true" \
             "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID" > /dev/null
      then
        echo "Squash was not ticked: enabled automatically on !$CI_MERGE_REQUEST_IID."
        exit 0
      fi
      echo "Merge requests into develop must be squashed: tick 'Squash commits when merge request is accepted', then run a new pipeline."
      exit 1
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop"
Enter fullscreen mode Exit fullscreen mode

The PUT call on /projects/:id/merge_requests/:iid with squash=true is the same as ticking the box in the UI; the CI_* variables fill in the URL automatically.

  • No action left for the developer.
  • The token is not protected: anyone who edits .gitlab-ci.yml in their branch can read it. The Developer role and a short expiry limit the risk.
  • MRs from forks do not receive the variable and fall back to blocking.
  • The "untick after the pipeline" loophole remains open.

Option 3 — Require a single commit in the MR

The job counts the MR's commits and fails if there is more than one: the developer squashes before merging. This is the equivalent of the GitHub Action, and the only way to close the "untick afterwards" loophole, since the check runs again on every push.

single-commit:
  stage: .pre
  image: public.ecr.aws/docker/library/alpine:3.20
  variables:
    GIT_DEPTH: 0  # full history, so the MR base commit is available
  script:
    - apk add --no-cache git > /dev/null
    - |
      HEAD_SHA="${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA:-$CI_COMMIT_SHA}"
      COMMITS=$(git rev-list --count "$CI_MERGE_REQUEST_DIFF_BASE_SHA..$HEAD_SHA")
      if [ "$COMMITS" -ne 1 ]; then
        echo "MRs into develop must contain a single commit (found $COMMITS). Squash it: git reset --soft origin/develop && git commit && git push --force-with-lease"
        exit 1
      fi
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop"
Enter fullscreen mode Exit fullscreen mode
  • GitLab does not expose the commit count as a variable: it is computed with git rev-list, hence GIT_DEPTH: 0.
  • CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is needed with merged results pipelines, where CI_COMMIT_SHA is a temporary merge commit.
  • Developers have to squash locally, then run git push --force-with-lease.

Comparison and recommendation

Recommendation: option 2, which removes forgotten checkboxes with no effort from developers and falls back to option 1 without a token. Option 3 is only worth it if the "untick afterwards" loophole is a real problem for the team.

Option 1 — Block Option 2 — Tick via the API Option 3 — Single commit
Box forgotten Merge blocked Box ticked automatically Not applicable
Developer effort Tick, then rerun None Squash locally
Box unticked after the pipeline Not detected Not detected Cannot be bypassed
Prerequisites None Token + CI variable GIT_DEPTH: 0
Added risk None Token readable by developers None

Difference with GitHub: GitHub can restrict the merge method per branch (ruleset), but has no squash checkbox readable in CI. GitLab is the opposite; only option 3 exists on both platforms.

See the Github: Require Squash Commits for Pull Requests to the “develop” Branch

Top comments (0)