Operations
Operational docs cover local credential handoff, repeatable tests, and the guardrails required to run git-pont safely.
Guides
Project Execution
Git CLI Credentials
GitPontGitCLI exists for apps like GitFolder that shell out to system git.
It should not perform sync operations. It only resolves provider credentials into a safe, non-interactive Git credential context.
Credential Context
GitPontGitCLI owns this type and adds the gitCredentialContext method as an extension on GitPont. GitPontCore must not import or depend on GitPontGitCLI.
public struct GitCLICredentialContext: Hashable, Sendable {
public var argumentsPrefix: [String] // e.g. ["-c", "credential.helper=..."]
public var environment: [String: String]
}
This is the only shape. There is no separate helperConfig accessor; argumentsPrefix is prepended to the git argument list as-is.
Usage:
let context = try await gitPont.gitCredentialContext(
forRemoteURL: remoteURL,
preferredConnectionID: folder.connectionID
)
let result = try gitRunner.run(
context.argumentsPrefix + ["ls-remote", "--heads", remoteURL.absoluteString],
environment: context.environment
)
HTTPS Token Auth
For HTTPS remotes, prefer an inline credential helper with an environment variable, not embedding tokens in remote URLs.
Example shape:
git -c credential.helper='!f() { printf "username=%s\npassword=%s\n" "$GITPONT_USERNAME" "$GITPONT_TOKEN"; }; f' ...
Environment:
GITPONT_USERNAME=...
GITPONT_TOKEN=...
GIT_TERMINAL_PROMPT=0
The token value appears only in environment, never in argumentsPrefix. The helper script text in argumentsPrefix references the variables by name.
If the credential for the connection is an expiring OAuth token, the facade refreshes it before building the context (see Authentication → Token Refresh). Contexts are single-use: request a fresh context per sync run rather than caching one.
Provider username defaults:
- GitHub OAuth/PAT:
x-access-token - GitLab OAuth:
oauth2; GitLab PAT: the account login (or any non-empty username; GitLab accepts the PAT as password) - Forgejo/Gitea/Codeberg: account login when known, otherwise token-compatible placeholder
Provider implementations should own these details.
Remote URL Matching
Given a remote URL:
https://github.com/owner/repo.git
https://gitlab.com/group/project.git
https://gitlab.company.com/group/project.git
https://codeberg.org/owner/repo.git
GitPont should:
- Normalize the URL (strip
.git, trailing slashes, credentials in the URL if present). - Resolve the provider instance (known hosts first, then configured instances).
- Find a connection for the instance, honoring
preferredConnectionID; multiple matches without a preference throw.ambiguousConnection. - Load credentials, refreshing if needed.
- Return CLI context.
If no matching connection exists, throw .missingConnection.
No SSH in v1
Do not implement SSH credential handling in GitPont v1.
GitFolder can keep its existing advanced SSH path if desired, but provider-based account connections should be the default and documented route.
Testing Strategy
git-pont must be heavily tested because provider integrations break easily and app data loss would be unacceptable.
Test Layers
- Pure model tests
- URL parser tests (including ambiguity)
- Request construction tests
- Response decoding tests
- Pagination tests
- Error mapping tests
- Credential and connection store tests
- Token refresh tests
- Git CLI credential tests
- Provider contract tests with mocked HTTP
- Change submission (orchestration) tests
- Optional live integration tests gated by environment variables
Mocked HTTP
Core defines the injectable HTTPClient (see Architecture → HTTP Abstraction). Tests use a MockHTTPClient that matches requests by method + URL pattern and replays fixture responses, recording every request for assertion.
Tests should not hit the network by default.
Use fixture-driven tests. SwiftPM keeps resources per test target, so each provider target owns its provider fixture files:
libs/swift/Tests/GitPontGitHubTests/Fixtures/GitHub/read-file.json
libs/swift/Tests/GitPontGitHubTests/Fixtures/GitHub/list-directory.json
libs/swift/Tests/GitPontGitLabTests/Fixtures/GitLab/read-file.json
libs/swift/Tests/GitPontGitLabTests/Fixtures/GitLab/list-directory.json
libs/swift/Tests/GitPontForgeTests/Fixtures/Forgejo/read-file.json
libs/swift/Tests/GitPontForgeTests/Fixtures/Forgejo/list-directory.json
Add new provider response cases as JSON fixtures where the payload is reusable across future Android, web, or backend kits. Keep request-only assertions inline when there is no response payload worth sharing.
URL Parser Matrix
Test at least:
- GitHub blob URL
- GitHub blob URL with commit-SHA permalink
- GitHub blob URL with
#L10fragment and query string (stripped) - GitHub raw URL (including
refs/heads/form) - GitHub tree (directory) URL
- GitHub repo URL, with and without
.git - GitHub blob URL with slashed branch →
.ambiguouswith correct candidate order - GitLab blob URL
- GitLab raw URL (
/-/raw/) - GitLab subgroup blob URL
- GitLab self-hosted blob URL (configured instance)
- GitLab repo URL
- Codeberg
src/branch/URL - Codeberg
src/commit/{sha}URL - Forgejo custom source URL
- Gitea custom source URL
- invalid URL
- unsupported host
- custom host without configured instance
Resolution tests: .ambiguous result + mocked branch list → correct (ref, path) chosen; no matching branch → .unsupportedURL.
Pagination Tests
- Multi-page branch and repository lists are concatenated (GitHub
Linkheader, GitLabx-next-page). - Safety cap sets
truncated = trueand stops requesting. per_page=100is sent on list requests.
Conflict Tests
Test that commits include provider version identity:
- GitHub sends
sha - GitLab sends
last_commit_id - Forgejo/Gitea sends
sha
Test provider conflict responses map to GitPontError.conflict(...) with a populated GitConflict.
Test that GitCommitResult.newVersion is populated after a successful commit for every provider (including GitLab's follow-up read).
Change Submission Tests
With mocked HTTP:
.directCommitperforms one commit call..branchAndPullRequestcreates branch, commits, opens PR; PR request uses the right head format..forkAndPullRequestcreates or reuses the connected account's fork, confirms newly created forks, commits to the fork, and opens PR on upstream with fork head (owner:branch/target_project_id)..automaticpicks direct commit whenpermissions.canPushand the branch is not protected; branch+PR when push but protected/PR requested; fork+PR whencanPush == false.- Commit success + PR failure →
.partialSubmissioncarrying the commit result. - Branch success + commit failure →
.partialSubmissionnaming the branch.
Credential and Connection Tests
Use in-memory stores in core tests:
final actor InMemoryCredentialStore: CredentialStore
final actor InMemoryConnectionStore: ConnectionStore
Keychain tests live in GitPontKeychainTests. The default suite verifies the credential payload round trip. A live Keychain save/load/update/delete test is opt-in with GITPONT_RUN_KEYCHAIN_TESTS=1.
Test:
- save/load/delete for both stores
- token not serialized in connection metadata (encode
GitConnection, assert no token substring) - missing credential returns
nil removeConnectiondeletes both metadata and credential
Token Refresh Tests
- Expiring credential triggers refresh before the request; refreshed token is used.
- Concurrent requests against one expiring connection produce exactly one refresh call (assert via mock request count).
- Refreshed credential is saved to the store before dependent requests run.
- Git CLI credential context uses a refreshed token for expiring OAuth credentials.
- Refresh failure does not delete the connection.
- Unexpected provider authentication failure triggers one refresh-and-retry when a refresh token exists.
Git CLI Credential Tests
Test that:
- tokens are placed in environment, not command arguments (assert token value absent from every
argumentsPrefixelement) GIT_TERMINAL_PROMPT=0is set- GitHub, GitLab, and Forgejo contexts use provider-appropriate usernames
- unknown provider returns
.unsupportedCapability - missing connection returns
.missingConnection - multiple connections without
preferredConnectionIDreturns.ambiguousConnection; with it, the preferred one is used
Live Tests
Live tests are disabled by default and require explicit environment variables. The checked-in live suite performs account smoke tests through URLSessionHTTPClient when a token is present:
GITPONT_LIVE_GITHUB_TOKEN
GITPONT_LIVE_GITLAB_TOKEN
GITPONT_LIVE_FORGEJO_TOKEN
GITPONT_LIVE_FORGEJO_BASE_URL # optional, defaults to Codeberg
The checked-in live suite also includes opt-in disposable write cycles. These create a temporary branch, create/read/delete a file under .git-pont-live/, and delete the temporary branch. Configure them only with dedicated test repositories:
GITPONT_LIVE_GITHUB_WRITE_REPO # owner/repo
GITPONT_LIVE_GITHUB_WRITE_BASE_REF # optional, defaults to main
GITPONT_LIVE_GITLAB_WRITE_REPO # namespace/project
GITPONT_LIVE_GITLAB_WRITE_BASE_REF # optional, defaults to main
GITPONT_LIVE_FORGEJO_WRITE_REPO # owner/repo
GITPONT_LIVE_FORGEJO_WRITE_BASE_REF # optional, defaults to main
Never run live write tests in normal CI unless a dedicated test repo is configured.
Use npm run validate:live as the release gate for all configured live provider write cycles. It fails fast when any required token or disposable repository variable is missing.
Safety Tests
Add tests that prevent common data-loss behavior:
- updating an existing file without expected version and without
allowBlindOverwritethrows.conflict(no request sent) - deleting without expected version and without
allowBlindOverwriteis rejected - path traversal segments (
..) are rejected - absolute file paths are rejected as repository paths
- NUL/control characters in paths are rejected
- empty commit message is rejected
- delete requires the explicit delete API; empty-content commit is not a delete
- files above the provider size limit throw
.fileTooLarge, not a decode error