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

89 lines
4.8 KiB
Markdown

# 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
- [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)