Files
werkator/docs/prs/2026-10-05-PR#122-git-credentials-and-pull-request-reading.md
miandmhoennig 31c73d3fb2
werkator build succeeded after 02:52
werkator/deploy build succeeded after 00:09
Git credentials and the reading of pull requests split up and tested, gate down to 28 (v1.37.3) (#122)
> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.

## The Problem

Two methods of the git and Gitea access are above the Kotlin target of 15:

| Method                                           | Mutation-CRAP | Complexity | Mutation score |
|--------------------------------------------------|---------------|------------|----------------|
| `GitService.authenticated`                       | 30.12         | 6          | 0.13           |
| `GiteaClient.GiteaPullRequest.toOpenPullRequest` | 16.04         | 16         | 0.95           |

`authenticated` decides which remote git commands get the configured credentials.
No test looked at that decision: the git tests run against local fixture repositories, whose origin is a path, so every fetch ran without credentials and 7 of 8 mutants survived.
`toOpenPullRequest` maps Gitea's answer and is well tested, but every absent field is a branch of its own.

## Non-Goals

- No change in behaviour.
- The other groups above 15 follow in their own PRs.

## The Scenarios

### Feature: remote git commands get credentials only where they can work

#### Scenario#122.01: only an HTTP(S) origin with account and token fetches through the askpass script

So that the token reaches git exactly where git can use it, and never a prompt blocks a fetch.

- **Given** a repository whose top level configures `git.account` and `git.token`
- **When** its origin is an HTTPS or an HTTP URL
- **Then** the fetch runs with `GIT_ASKPASS`, `WERKATOR_GIT_ACCOUNT`, `WERKATOR_GIT_TOKEN` and `GIT_TERMINAL_PROMPT=0`
  - **and** the credentials are read from the repository's top level, also when the command runs in a subdirectory
  - **and** the askpass script exists while the command runs and is gone afterwards
- **When** the origin is an SSH URL or there is no origin
- **Then** the fetch runs with `GIT_TERMINAL_PROMPT=0` alone, and no configuration is read
- **When** the account or the token is blank
- **Then** the fetch runs with `GIT_TERMINAL_PROMPT=0` alone

##### Verified by

- [GitServiceCredentialsUnitTest](../../src/test/kotlin/de/hoennig/werkator/git/GitServiceCredentialsUnitTest.kt)

### Feature: open pull requests are read whatever Gitea leaves out

#### Scenario#122.02: a pull request without head or base reads as a fork, and the title prefix is matched in any case

So that a sparse or older answer never marks a foreign branch as the repository's own, nor misses a draft.

- **Given** Gitea answers a pull request without `head` and `base`, titled `[wip] lower case`
  - **and** one whose head has no `repo_id`
  - **and** one whose title only mentions `WIP:` in its middle
- **When** the open pull requests are read
- **Then** the first is a draft from a fork with empty refs
  - **and** the second is from a fork, since nothing says its branch is the repository's own
  - **and** the third is no draft

##### Verified by

- [GiteaClientIntegrationTest](../../src/test/kotlin/de/hoennig/werkator/gitea/GiteaClientIntegrationTest.kt) `a pull request without head or base reads as a fork, and the title prefix is matched in any case (Scenario#122.02)`, and the Scenario#117.02 test

## The Solution

`GitService.credentialsFor` answers which credentials a repository's remote commands get, null for none; `authenticated` runs the command with the askpass script or with `NO_PROMPT`.
`GiteaPullRequest.toOpenPullRequest` replaces an absent head or base with an empty branch once, the branch answers its ref and commit itself, and `isDraft` holds the fallback on the title.

A local PIT run of `GitService` and `GiteaClient` afterwards:

| Method                                           | Mutation-CRAP | Complexity | Mutation score |
|--------------------------------------------------|---------------|------------|----------------|
| `GitService.authenticated`                       | 2.15          | 2          | 0.67           |
| `GitService.credentialsFor`                      | 9             | 9          | 1.00           |
| `GiteaClient.GiteaPullRequest.toOpenPullRequest` | 6             | 6          | 1.00           |
| `GiteaClient.GiteaPullRequest.isDraft`           | 3.03          | 3          | 0.86           |

The highest score left in both classes is 12 (`GitService.originBranchCommitTimes`).
The project maximum is now `NginxProxyManager.resolveSettings` at 27.44, so the gate goes down from 31 to 28.

## Additional Changes

- `maxMutationCrap` in `build.gradle.kts` lowered from 31 to 28.
- Version 1.37.3 with its entry in `releases.html`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #122
2026-10-05 18:30:38 +02:00

4.8 KiB

PR#122: Git Credentials and the Reading of Pull Requests Split Up and Tested

WARNING: This document describes only the change applied in this PR. It may already be outdated once the next PR is merged. Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.

The Problem

Two methods of the git and Gitea access are above the Kotlin target of 15:

Method Mutation-CRAP Complexity Mutation score
GitService.authenticated 30.12 6 0.13
GiteaClient.GiteaPullRequest.toOpenPullRequest 16.04 16 0.95

authenticated decides which remote git commands get the configured credentials. No test looked at that decision: the git tests run against local fixture repositories, whose origin is a path, so every fetch ran without credentials and 7 of 8 mutants survived. toOpenPullRequest maps Gitea's answer and is well tested, but every absent field is a branch of its own.

Non-Goals

  • No change in behaviour.
  • The other groups above 15 follow in their own PRs.

The Scenarios

Feature: remote git commands get credentials only where they can work

Scenario#122.01: only an HTTP(S) origin with account and token fetches through the askpass script

So that the token reaches git exactly where git can use it, and never a prompt blocks a fetch.

  • Given a repository whose top level configures git.account and git.token
  • When its origin is an HTTPS or an HTTP URL
  • Then the fetch runs with GIT_ASKPASS, WERKATOR_GIT_ACCOUNT, WERKATOR_GIT_TOKEN and GIT_TERMINAL_PROMPT=0
    • and the credentials are read from the repository's top level, also when the command runs in a subdirectory
    • and the askpass script exists while the command runs and is gone afterwards
  • When the origin is an SSH URL or there is no origin
  • Then the fetch runs with GIT_TERMINAL_PROMPT=0 alone, and no configuration is read
  • When the account or the token is blank
  • Then the fetch runs with GIT_TERMINAL_PROMPT=0 alone
Verified by

Feature: open pull requests are read whatever Gitea leaves out

Scenario#122.02: a pull request without head or base reads as a fork, and the title prefix is matched in any case

So that a sparse or older answer never marks a foreign branch as the repository's own, nor misses a draft.

  • Given Gitea answers a pull request without head and base, titled [wip] lower case
    • and one whose head has no repo_id
    • and one whose title only mentions WIP: in its middle
  • When the open pull requests are read
  • Then the first is a draft from a fork with empty refs
    • and the second is from a fork, since nothing says its branch is the repository's own
    • and the third is no draft
Verified by
  • GiteaClientIntegrationTest a pull request without head or base reads as a fork, and the title prefix is matched in any case (Scenario#122.02), and the Scenario#117.02 test

The Solution

GitService.credentialsFor answers which credentials a repository's remote commands get, null for none; authenticated runs the command with the askpass script or with NO_PROMPT. GiteaPullRequest.toOpenPullRequest replaces an absent head or base with an empty branch once, the branch answers its ref and commit itself, and isDraft holds the fallback on the title.

A local PIT run of GitService and GiteaClient afterwards:

Method Mutation-CRAP Complexity Mutation score
GitService.authenticated 2.15 2 0.67
GitService.credentialsFor 9 9 1.00
GiteaClient.GiteaPullRequest.toOpenPullRequest 6 6 1.00
GiteaClient.GiteaPullRequest.isDraft 3.03 3 0.86

The highest score left in both classes is 12 (GitService.originBranchCommitTimes). The project maximum is now NginxProxyManager.resolveSettings at 27.44, so the gate goes down from 31 to 28.

Additional Changes

  • maxMutationCrap in build.gradle.kts lowered from 31 to 28.
  • Version 1.37.3 with its entry in releases.html.

🤖 Generated with Claude Code