Help us shape the future of Go! Take the 2026 Go Developer Survey

provider

package
v0.99.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 30, 2026 License: MIT Imports: 30 Imported by: 4

Documentation

Overview

Package provider is the public API of go-git-platform: one Go interface for GitHub, GitLab, Gitea, Forgejo, Gitee, GitCode, and Tencent Code (工蜂), plus the local-git entry points that hang off the same unified models.

Overview

Start with NewProvider and the blank import that registers every shipped backend:

import _ "github.com/yi-nology/go-git-platform/backends/all"

p, err := provider.NewProvider(provider.Config{
    Platform: provider.PlatformGitHub,
    Token:    "ghp_...",
})

The returned Provider composes eight core sub-interface families — repositories, change requests (PRs/MRs), webhooks, branches, diffs, commits, files, and releases — so consumer code can depend on the narrow slice it needs. Thirteen further capability families (issues, search, labels, milestones, reviews, commit statuses, notifications, reactions, branch protections, collaborators, deploy keys, repo stats, users) are optional: backends declare them via Capabilities() and consumers reach them with a type assertion.

Configuration surface

Config carries everything a backend needs: platform, base URL (for self-hosted instances), credentials, retry, hooks, and logging. Two newer knobs are worth knowing:

  • Config.TokenSource replaces the static Token with a per-request source, so expiring credentials (GitHub App installation tokens, OAuth flows) rotate without rebuilding the provider.
  • Config.ConditionalRequests enables ETag-based conditional GETs: the transport replays cached 304 answers, which on GitHub do not count against the rate-limit budget — a large win for polling workloads like WaitForCommitStatus.

Cross-platform consistency

Behavior parity is enforced, not aspirational: backends/contracttest runs the same suite against every platform, and every deliberate departure from unified semantics is registered in the backend's Divergence ledger (rendered to docs/divergence-ledger.md). The webhook event corpus (testdata/webhooks under each backend) pins the normalized event vocabulary — cr./push/tag./branch./issue./comment. — across platforms with golden files.

Automation helpers

Beyond the raw managers, the package ships primitives for agent and CI use: WaitForCommitStatus (CI gating), GetFileContents (bounded concurrency batch reads), Each/Collect (page-walking iterators), EnsureWebhook/EnsureBranchProtection/EnsureDeployKey (idempotent desired-state convergence), and RateLimitRecovery (server-advised backoff hints from rate-limit errors).

Index

Constants

View Source
const (
	ReactionPlusOne  = "+1"
	ReactionMinusOne = "-1"
	ReactionLaugh    = "laugh"
	ReactionConfused = "confused"
	ReactionHeart    = "heart"
	ReactionHooray   = "hooray"
	ReactionRocket   = "rocket"
	ReactionEyes     = "eyes"
)

Standard emoji identifiers used across all platforms. GitLab maps these to its award-emoji names internally (e.g. +1 ↔ thumbsup).

View Source
const (
	DefaultPage    = 1
	DefaultPerPage = 20
	MaxPerPage     = 100
)
View Source
const (
	DefaultWaitInterval = 5 * time.Second
	DefaultWaitTimeout  = 10 * time.Minute
)

Default polling parameters for WaitForCommitStatus.

View Source
const (
	// CR (change request / pull request / merge request) actions.
	CRActionOpened   = "opened"
	CRActionClosed   = "closed"
	CRActionMerged   = "merged"
	CRActionReopened = "reopened"
	CRActionUpdated  = "updated"

	// Event type prefixes.
	EventTypeCR      = "cr."
	EventTypePush    = "push"
	EventTypeTag     = "tag."
	EventTypeBranch  = "branch."
	EventTypeIssue   = "issue."
	EventTypeComment = "comment."

	// Comment actions. GitHub delivers created/edited/deleted; the other
	// platforms only notify on creation today.
	CommentActionCreated = "created"
	CommentActionEdited  = "edited"
	CommentActionDeleted = "deleted"
)
View Source
const DefaultBatchConcurrency = 4

DefaultBatchConcurrency is the number of in-flight operations used by the batch helpers when BatchOptions.Concurrency is unset.

View Source
const DefaultGiteeSignMaxAge = 10 * time.Minute

DefaultGiteeSignMaxAge bounds the accepted X-Gitee-Timestamp drift in sign mode when GiteeWebhookValidator.MaxAge is zero.

View Source
const DefaultIterMaxPages = 100

DefaultIterMaxPages is the page budget applied by Each/Collect when no explicit bound is given. At the default page size (20) this caps a walk at 2000 items; with PerPage=100 it caps at 10000 — far beyond any real list. The budget is a safety net against platforms that ignore the page parameter (observed in the field: Forgejo 15.0.1 on issue-comment lists), so exhausting it is surfaced as ErrPageBudgetExceeded instead of spinning.

View Source
const MaxBatchConcurrency = 16

MaxBatchConcurrency caps the caller-configurable concurrency so a misconfigured batch cannot hammer a platform into rate limiting.

View Source
const SkipValidationSecret = "\x00skip-signature-validation"

SkipValidationSecret is the sentinel secret that makes WebhookEnvelope.Read skip the platform's signature validator.

It exists for hosts that verify the webhook signature themselves and then reuse the SDK purely as a payload parser. Two situations need it:

  • The platform's header convention differs from the SDK's model (e.g. a GitLab-family payload delivered by a platform that brands its own token header).
  • The host validates the signature before dispatch, so the SDK's validation would be a redundant second check on an already-trusted request.

The sentinel is intentionally unusable as a real secret: it contains a NUL byte, which no HTTP header value or config file can carry. It is honoured only inside WebhookEnvelope.Read; ValidateWebhookWithRegistry and Provider.ValidateWebhookSignature always run the real validator, so passing the sentinel there does NOT bypass signature checking.

Variables

View Source
var (
	ErrNotFound             = errors.New("resource not found")
	ErrAuthentication       = errors.New("authentication failed")
	ErrRateLimited          = errors.New("rate limited")
	ErrForbidden            = errors.New("forbidden")
	ErrConflict             = errors.New("conflict")
	ErrNotImplemented       = errors.New("not implemented")
	ErrInvalidInput         = errors.New("invalid input")
	ErrWebhookValidation    = errors.New("webhook validation failed")
	ErrConnectionFailed     = errors.New("connection failed")
	ErrPlatformNotSupported = errors.New("platform not supported")
)

Sentinel errors classify transport and provider failures so callers can branch on category without inspecting status codes or wrapped causes.

View Source
var ErrPageBudgetExceeded = oekiterrors.New("page budget exceeded before an empty page")

ErrPageBudgetExceeded is returned when the walk hits its MaxPages bound before the platform returns an empty page. Either the list is genuinely longer than the budget, or (more likely) the platform ignores the page parameter. Retry with a higher bound via EachBounded/CollectBounded if the former.

View Source
var ErrStopIteration = oekiterrors.New("iteration stopped")

ErrStopIteration is the sentinel a Each callback returns to stop the walk early without reporting an error. Everything fetched so far stands.

View Source
var ErrWaitTimedOut = oekiterrors.New("commit status did not reach a terminal state before the timeout")

ErrWaitTimedOut is returned by WaitForCommitStatus when the commit stays non-terminal past WaitOptions.Timeout.

Functions

func BuildRawDiff

func BuildRawDiff(files []*ChangedFile) string

BuildRawDiff constructs a raw diff string from a list of ChangedFiles.

func ClassifyStatus

func ClassifyStatus(statusCode int) error

ClassifyStatus maps an HTTP status code to a sentinel error. Exposed so transports and platform implementations can produce consistent error categories without importing each other.

func Collect added in v0.72.0

func Collect[T any](ctx context.Context, fetch PageFetch[T]) ([]T, error)

Collect walks every page of a paginated list call and returns all items as one slice. It is the convenient counterpart of Each for callers that want the whole list in memory.

func CollectBounded added in v0.72.0

func CollectBounded[T any](ctx context.Context, fetch PageFetch[T], maxPages int) ([]T, error)

CollectBounded is Collect with an explicit page budget (maxPages <= 0 falls back to DefaultIterMaxPages).

func CountDiffLines

func CountDiffLines(diff string) (additions, deletions int)

CountDiffLines counts additions and deletions in a unified diff string.

func DecodeFileContent added in v0.99.7

func DecodeFileContent(platform Platform, encoding, content string) (string, error)

DecodeFileContent 按平台返回的 Encoding 解码文件内容。

平台的「取文件内容」端点有两种形态:raw(直接给字节)与 JSON(base64 字段)。 **不按 Encoding 解码 ⇒ 调用方拿到 base64 串** —— 按 GitHub 直觉读文件会全错 (解析 YAML/JSON 直接失败),而调用方无从察觉。

只认 `base64`(大小写不敏感);其它 encoding 原样返回。

func Each added in v0.72.0

func Each[T any](ctx context.Context, fetch PageFetch[T], fn func(T) error) error

Each walks every page of a paginated list call, invoking fn once per item, until the platform returns an empty page. Pages are fetched lazily: fn sees items page by page, so a walk over a long list does not buffer it in memory. Use Collect instead when the full slice is wanted.

fn returning ErrStopIteration ends the walk with a nil error; any other error aborts immediately and is returned as-is. ctx cancellation between items and between page fetches is honored.

func EachBounded added in v0.72.0

func EachBounded[T any](ctx context.Context, fetch PageFetch[T], maxPages int, fn func(T) error) error

EachBounded is Each with an explicit page budget. maxPages <= 0 falls back to DefaultIterMaxPages. Exceeding the budget fails the walk with an error wrapping ErrPageBudgetExceeded (unlike the internal backend helper, a public walk must not truncate silently).

func EnsureBranchProtection added in v0.72.0

func EnsureBranchProtection(ctx context.Context, bpm BranchProtectionManager, owner, repo string, opts CreateBranchProtectionOptions) (EnsureAction, *BranchProtection, error)

EnsureBranchProtection converges the branch protection rule for opts.BranchName onto opts, creating, updating, or leaving it untouched as required, and returns the resulting rule plus the action taken. It targets the BranchProtectionManager directly so it works with any Provider (type-assert it at the call site) or test double.

func EnsureDeployKey added in v0.72.0

func EnsureDeployKey(ctx context.Context, dkm DeploymentKeyManager, owner, repo string, opts AddDeployKeyOptions) (EnsureAction, *DeployKey, error)

EnsureDeployKey converges the repository's deploy key titled opts.Title onto opts, creating, replacing, or leaving it untouched as required, and returns the resulting key plus the action taken.

Existing keys are matched by title; a title collision with different key material or read-only flag replaces the key (the manager interface has no update: delete + re-add), which changes its platform ID.

func EnsureWebhook added in v0.72.0

EnsureWebhook makes the repository carry exactly one webhook for opts.URL with opts.Events, creating or replacing as needed, and returns the resulting webhook plus the action taken. It is the idempotent building block automation wants: "make this hook exist, whatever it takes", safe to re-run.

Semantics:

  • No webhook with that URL → created.
  • Same URL, same event set → unchanged. Secrets are write-only on every platform (list responses never return them), so an existing hook's secret is not comparable; re-running with a different secret reports unchanged and keeps the old secret.
  • Same URL, different event set → updated. The WebhookManager interface has no update method, so the hook is deleted and recreated; its platform ID changes as a result.
  • Empty opts.Events defaults to {push, pull_request}, matching CreateWebhook's platform-side default.

Multiple existing hooks with the same URL are all replaced by one.

func ExtractMentions

func ExtractMentions(body string) []string

ExtractMentions returns the deduplicated list of @usernames found in body. Email addresses (foo@bar.com) are excluded by the leading non-word-char guard. Mentions inside URLs (e.g. `[@user](https://example.com/@user)`) are also excluded. The returned order follows first occurrence.

func ExtractOwnerFromFullName

func ExtractOwnerFromFullName(fullName string) string

ExtractOwnerFromFullName returns just the owner portion of "owner/repo".

func HashToken

func HashToken(token string) string

HashToken returns the first 16 hex characters of SHA-256(token). It is the default token hasher used by Manager and is exported so callers can compute cache keys for diagnostics or external caches.

An empty token hashes to an empty string (no anonymous cache entries).

func Ignores

func Ignores(divs []Divergence, method, field string) bool

Ignores reports whether the ledger registers an ignore of field on method.

func IsAuthentication

func IsAuthentication(err error) bool

IsAuthentication reports whether err wraps ErrAuthentication (HTTP 401).

func IsConflict

func IsConflict(err error) bool

IsConflict reports whether err wraps ErrConflict (HTTP 409).

func IsForbidden

func IsForbidden(err error) bool

IsForbidden reports whether err wraps ErrForbidden (HTTP 403).

func IsInvalidInput

func IsInvalidInput(err error) bool

IsInvalidInput reports whether err wraps ErrInvalidInput.

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports whether err wraps ErrNotFound (HTTP 404).

func IsNotImplemented

func IsNotImplemented(err error) bool

IsNotImplemented reports whether err wraps ErrNotImplemented.

func IsPlatformNotSupported

func IsPlatformNotSupported(err error) bool

IsPlatformNotSupported reports whether err wraps ErrPlatformNotSupported.

func IsRateLimited

func IsRateLimited(err error) bool

IsRateLimited reports whether err wraps ErrRateLimited (HTTP 429).

func IsRegistered

func IsRegistered(p Platform) bool

IsRegistered checks if a platform has been registered.

func IsWebhookValidation

func IsWebhookValidation(err error) bool

IsWebhookValidation reports whether err wraps ErrWebhookValidation.

func New

func New(platform Platform, op string, status int, body string) error

New builds a ProviderError directly from the given status code.

func NormalizeBranchAction

func NormalizeBranchAction(action string) string

NormalizeBranchAction maps platform-specific branch event actions.

func NormalizeCRAction

func NormalizeCRAction(action string, merged bool) string

NormalizeCRAction maps platform-specific PR/MR action strings to the canonical vocabulary. Each backend should call this instead of ad-hoc string mapping.

Mappings:

GitHub:   "opened" → opened, "closed" (+merged) → merged/closed,
          "reopened" → reopened, "synchronize"/"edited" → updated
GitLab:   "open" → opened, "close" → closed, "merge" → merged,
          "reopen" → reopened, "update" → updated
Gitee:    "open" → opened, "close" (+merged check) → merged/closed
GitCode:  "opened" → opened, "closed" (+merged check) → merged/closed
Gitea:    "opened" → opened, "closed" (+merged check) → merged/closed

func NormalizeIssueAction

func NormalizeIssueAction(action string) string

NormalizeIssueAction maps platform-specific issue event actions.

func NormalizePageOpts

func NormalizePageOpts(page, perPage int) (int, int)

NormalizePageOpts applies default values for page/perPage.

func NormalizeTagAction

func NormalizeTagAction(action string) string

NormalizeTagAction maps platform-specific tag event actions to the canonical vocabulary. The canonical full event type is "tag.created": GitLab/Gitea/Tencent signal a tag with a tag_push hook (a tag being pushed is the tag coming into existence), and GitHub's create/delete events carry ref_type "tag".

func ParseTotalCountHeader

func ParseTotalCountHeader(headers http.Header, fallback int) int

ParseTotalCountHeader reads X-Total-Count or X-Total from response headers. Falls back to the provided default if neither header is present or valid.

func RateLimitRecovery added in v0.72.0

func RateLimitRecovery(err error) (retryAfter time.Duration, resetAt time.Time, ok bool)

RateLimitRecovery extracts the server-advised recovery window from a rate-limited error, walking the wrapped chain for the transport hints (Retry-After / X-RateLimit-Reset). ok is true only when at least one of retryAfter or resetAt is known — callers rate-limited without hints should fall back to their own backoff.

if ra, reset, ok := provider.RateLimitRecovery(err); ok {
    wait := ra
    if reset.After(time.Now()) && (wait == 0 || time.Until(reset) > wait) {
        wait = time.Until(reset)
    }
    time.Sleep(wait)
}

func ReadAndRestoreBody

func ReadAndRestoreBody(r *http.Request) ([]byte, error)

ReadAndRestoreBody reads the full body of r and replaces it with a fresh NopCloser so that downstream readers (signature verification, JSON decoding) can still consume it. Use it at the entry point of webhook handlers that need to inspect the raw bytes.

func Register

func Register(p Platform, ctor ProviderConstructor)

Register registers a provider constructor for a platform. This is typically called from init() functions in platform implementation files.

func RegisterHostAlias added in v0.77.0

func RegisterHostAlias(host string, platform Platform, baseURL string)

RegisterHostAlias teaches DetectPlatform that host (and its subdomains) identifies platform with the given base API URL — the registry extension matching Register: adding a self-hosted alias no longer requires editing the provider core. Panics on an empty host, an empty platform, or a duplicate host (same fail-fast convention as Register).

func RegisterStatusExtractor added in v0.83.0

func RegisterStatusExtractor(p Platform, fn StatusExtractor)

RegisterStatusExtractor teaches Wrap how to read the HTTP status out of the named platform's SDK error values — the explicit, type-safe replacement for the old reflection + error-message-parsing fallback. Backends register one extractor at init covering their SDK's error types via errors.As; a nil extractor (or no registration) means the platform's errors carry no status on the error value itself (use WrapStatusError / New at the call site, or thread the *Response status through the port as the family backends do). Panics on duplicate registration (same convention as Register).

func ResolveMRSHAs

func ResolveMRSHAs(diffRefsHead, diffRefsBase, diffRefsStart, mergeCommitSHA, lastCommitID string) (head, base, start string)

ResolveMRSHAs derives the (head, base, start) SHAs for a GitLab-style merge request from the raw webhook fields. This encodes the shared priority used by the GitLab, TencentCode (and future GitCode) backends:

  • head: diff_refs.head_sha when present, otherwise last_commit.id
  • base: merge_commit_sha when present, otherwise diff_refs.base_sha
  • start: diff_refs.start_sha (may be empty)

Keeping this in one place guarantees identical fallback semantics across the GitLab-family backends and avoids behavioural drift.

func SplitFullName

func SplitFullName(fullName string) (owner, name string)

SplitFullName splits "owner/repo" into (owner, repo). If the input doesn't contain "/", owner is empty.

func Stubs

func Stubs(divs []Divergence, method string) bool

Stubs reports whether the ledger registers method as a stub.

func SumDiffStats

func SumDiffStats(files []*ChangedFile) (additions, deletions int)

SumDiffStats returns total additions and deletions from a list of ChangedFiles.

func ValidateWebhookWithRegistry added in v0.77.0

func ValidateWebhookWithRegistry(p Platform, r *http.Request, secret string) error

ValidateWebhookWithRegistry validates req's webhook signature with the default registry entry for platform — the single implementation per signature scheme (STRATEGY dispatch). The body is read and restored so callers can keep parsing the payload afterwards. Platforms without a registered validator yield ErrNotImplemented; an empty secret is rejected (an empty HMAC key is trivially forgeable on predictable payloads).

Backend WebhookManager implementations delegate here instead of keeping an inline copy of their platform's crypto, which had already drifted from the registry (empty-secret semantics) before v0.77.0.

func Version added in v0.72.0

func Version() string

Version returns the runtime version of the go-git-platform module, as resolved from Go build info. It reports "dev" for development builds (in-module go run/go test) where no module version is stamped.

The value is also injected as part of the default User-Agent header on every outgoing HTTP request made through the transport layer, so platform operators can identify (and, where offered, rate-limit-mercy) SDK-driven traffic.

func Wrap

func Wrap(platform Platform, op string, err error) error

Wrap creates a ProviderError from a raw error, classifying it when the cause is a transport error with a known status code. The original error chain is always preserved: Cause stays rooted at the raw error (with the classification sentinel wrapped in front via %w), so errors.Is matches both the sentinel (ErrNotFound & co.) and any error from the original chain, and the Error() message retains the platform's original detail. Use this in platform implementations to convert transport errors into the unified shape.

func WrapStatusError

func WrapStatusError(err error, statusCode int) error

WrapStatusError wraps an error with an explicit HTTP status code. This is the preferred way to attach status codes to third-party SDK errors instead of relying on reflection-based detection.

func Wrapf

func Wrapf(platform Platform, op, format string, args ...any) error

Wrapf is a convenience for creating a ProviderError with a formatted message. The format is intentionally simple ("%s/%s") so platform code can embed the resource identifier without re-implementing the prefix logic.

Types

type AddCollaboratorOptions

type AddCollaboratorOptions struct {
	Permission string `json:"permission,omitempty"` // "read", "write", "admin"
}

AddCollaboratorOptions contains options for adding a collaborator.

type AddDeployKeyOptions

type AddDeployKeyOptions struct {
	Title    string `json:"title"`
	Key      string `json:"key"`
	ReadOnly bool   `json:"read_only,omitempty"`
}

AddDeployKeyOptions contains options for adding a deploy key.

type BatchOptions

type BatchOptions struct {
	// Concurrency is the maximum number of in-flight operations;
	// defaults to DefaultBatchConcurrency, capped at
	// MaxBatchConcurrency.
	Concurrency int
}

BatchOptions tunes the batch helpers' concurrency. Zero fields take the defaults.

type BatchResult

type BatchResult[T any] struct {
	// Key is the input selector: the file path, CR number, or the
	// status context.
	Key string
	// Value carries the operation result when Err is nil.
	Value T
	// Err is the per-item failure, already wrapped as a provider error
	// by the underlying call.
	Err error
}

BatchResult pairs one batch item's outcome with its input key so callers can attribute per-item failures without re-deriving order. A per-item Err never aborts the whole batch.

func GetCRs

func GetCRs(ctx context.Context, p Provider, owner, repo string, numbers []string, batchOpts ...BatchOptions) []BatchResult[*ChangeRequest]

GetCRs fetches many change requests from one repository with bounded concurrency, in input order. A lookup failure for one number fails only that item.

func GetFileContents

func GetFileContents(ctx context.Context, p Provider, owner, repo, ref string, paths []string, batchOpts ...BatchOptions) []BatchResult[string]

GetFileContents reads many files from one repository with bounded concurrency. ref may be empty to use the repository's default branch (a single ref applies to the whole batch; per-path refs are expressible by calling the helper once per ref).

It is the context-economy read primitive for agent flows: fetch the handful of files that matter in one call, then trim each payload with projection if needed.

func SetCommitStatuses

func SetCommitStatuses(ctx context.Context, p Provider, owner, repo, sha string, statuses []CommitStatusOptions, batchOpts ...BatchOptions) []BatchResult[struct{}]

SetCommitStatuses reports many statuses on one commit with bounded concurrency (the usual "CI publishes lint+test+build at once" shape). The provider must implement CommitStatusManager; a provider without it yields every item failed with ErrNotImplemented.

type BranchManager

type BranchManager interface {
	ListBranches(ctx context.Context, owner, repo string) ([]*PlatformBranch, error)
	CreateBranch(ctx context.Context, owner, repo, branch, ref string) (*PlatformBranch, error)
	DeleteBranch(ctx context.Context, owner, repo, branch string) error
}

BranchManager handles branch operations.

type BranchProtection

type BranchProtection struct {
	BranchName               string `json:"branch_name"`
	RequiredApprovingReviews int    `json:"required_approving_reviews"`
	RequiredStatusChecks     bool   `json:"required_status_checks"`
	AllowForcePushes         bool   `json:"allow_force_pushes"`
	AllowDeletions           bool   `json:"allow_deletions"`
}

BranchProtection represents a branch protection rule.

type BranchProtectionManager

type BranchProtectionManager interface {
	// ListBranchProtections returns all branch protection rules for a repo.
	ListBranchProtections(ctx context.Context, owner, repo string) ([]*BranchProtection, error)
	// GetBranchProtection returns the protection rule for a specific branch.
	GetBranchProtection(ctx context.Context, owner, repo, branch string) (*BranchProtection, error)
	// CreateBranchProtection creates a new branch protection rule.
	CreateBranchProtection(ctx context.Context, owner, repo string, opts CreateBranchProtectionOptions) (*BranchProtection, error)
	// UpdateBranchProtection updates an existing branch protection rule.
	UpdateBranchProtection(ctx context.Context, owner, repo, branch string, opts UpdateBranchProtectionOptions) (*BranchProtection, error)
	// DeleteBranchProtection removes the protection rule for a branch.
	DeleteBranchProtection(ctx context.Context, owner, repo, branch string) error
}

BranchProtectionManager provides branch protection rule CRUD. It is an optional capability interface: consumers should gate on Provider.Capabilities().BranchProtections (or type-assert) before use.

Platform support: GitHub, GitCode, GitLab, Gitea, Forgejo, Gitee, TencentCode. TencentCode's list returns branch-row summaries (the platform has no list-protected endpoint; approver rules surface via GetBranchProtection), and allow-force-push is write-only there — see the tencentcode divergence ledger for the registered details. The platform- specific TencentCodeExtras still exposes the finer-grained protected- branch member management the unified contract does not model.

type CILogManager added in v0.67.0

type CILogManager interface {
	// ListFailedJobLogs returns the failed jobs of the most recent pipeline
	// for a commit, each with the tail of its execution log. A commit with
	// no pipeline (or a green one) yields an empty slice, not an error.
	ListFailedJobLogs(ctx context.Context, owner, repo, sha string) ([]JobLog, error)
}

CILogManager fetches failure diagnostics from a platform's CI system. It is an optional capability interface: consumers should type-assert before use. Only GitLab exposes per-job logs through its public REST API today; GitHub check-runs carry only summaries/annotations, so absence is expressed by not implementing the interface instead of stubbing.

type CRComment

type CRComment struct {
	ID        int64     `json:"id"`
	Body      string    `json:"body"`
	Author    *CRUser   `json:"author"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

CRComment represents a comment on a change request.

func (*CRComment) Mentions

func (c *CRComment) Mentions() []string

Mentions returns the deduplicated @usernames found in the comment body.

type CRCommit

type CRCommit struct {
	SHA       string    `json:"sha"`
	Message   string    `json:"message"`
	Author    *CRUser   `json:"author"`
	CreatedAt time.Time `json:"created_at"`
}

CRCommit represents a commit in a change request.

type CRState

type CRState string

CRState represents the state of a change request.

const (
	CRStateOpened CRState = "opened"
	CRStateMerged CRState = "merged"
	CRStateClosed CRState = "closed"
)

func MapBoolStateToCR

func MapBoolStateToCR(state string, merged bool) CRState

MapBoolStateToCR is a convenience for platforms with a separate merged boolean (Gitea, Forgejo).

func MapMRStateToCR

func MapMRStateToCR(state string) CRState

MapMRStateToCR is a convenience for platforms with explicit "merged" state (GitLab, Tencent Code).

func MapStateToCR

func MapStateToCR(state string, mergedFn func() bool) CRState

MapStateToCR maps common string state representations to CRState. mergedFn is called when state is "closed" to determine if it was merged. For platforms that use a separate "merged" field (Gitea, Forgejo), pass a non-nil mergedFn. For platforms where "merged" is a distinct state string (GitLab, Tencent Code), pass nil.

type CRUser

type CRUser struct {
	ID        int64  `json:"id"`
	Username  string `json:"username"`
	Name      string `json:"name"`
	AvatarURL string `json:"avatar_url"`
}

CRUser represents a user on a platform.

type CapabilitySet

type CapabilitySet struct {
	Issues            bool // provider.IssueManager
	Search            bool // provider.SearchManager
	Labels            bool // provider.LabelManager
	Milestones        bool // provider.MilestoneManager
	Reviews           bool // provider.ReviewManager
	CommitStatuses    bool // provider.CommitStatusManager
	Notifications     bool // provider.NotificationManager
	Reactions         bool // provider.ReactionManager
	BranchProtections bool // provider.BranchProtectionManager
	Collaborators     bool // provider.CollaboratorManager
	DeployKeys        bool // provider.DeploymentKeyManager
	RepoStats         bool // provider.RepoStatsManager
	Users             bool // provider.UserManager
	Gists             bool // provider.GistManager
	Starred           bool // provider.StarredManager
	Migrations        bool // provider.MigrationManager
	ReleaseAssets     bool // provider.ReleaseAssetManager
}

CapabilitySet statically declares which optional capability interfaces a Provider implements. Values are compile-time constants per backend; no runtime probing is performed. Consumers should route on these flags instead of probing with type assertions:

if p.Capabilities().Labels {
	lm := p.(provider.LabelManager)
	// ...
}

When a new optional capability interface is added to the SDK, add a field here and update every backend's Capabilities method; the contract suite enforces that declarations match implementations.

func DeriveCapabilities added in v0.82.0

func DeriveCapabilities(p Provider) CapabilitySet

DeriveCapabilities builds a CapabilitySet from the concrete provider's optional-manager implementations — the runtime type assertion is the SINGLE source of truth for what a backend supports. Backend Capabilities() methods delegate here, so the declaration can no longer drift from the implementation (contracttest's bidirectional check then guards against a backend bypassing the derivation with a hand-written literal).

This collapses the historical dual channel: pre-0.82 each backend wrote a literal of 12–17 bools that consumers cross-checked against assertions.

type ChangeRequest

type ChangeRequest struct {
	ID           int64   `json:"id"`
	Number       string  `json:"number"`
	Title        string  `json:"title"`
	Description  string  `json:"description"`
	State        CRState `json:"state"`
	SourceBranch string  `json:"source_branch"`
	TargetBranch string  `json:"target_branch"`
	// HeadSHA is the SHA of the source-branch tip.
	// BaseSHA is the diff base: the merge-base (common ancestor) where GitLab/TencentCode
	// expose it via diff_refs.base_sha. GitHub/Gitea do not expose a distinct merge base in
	// webhook payloads, so BaseSHA there is the target-branch tip (equivalent to StartSHA).
	// StartSHA is the SHA of the target-branch tip at event time (GitLab diff_refs.start_sha).
	// On GitHub/Gitea it equals BaseSHA since no separate value is exposed.
	HeadSHA  string `json:"head_sha,omitempty"`
	BaseSHA  string `json:"base_sha,omitempty"`
	StartSHA string `json:"start_sha,omitempty"`
	// Draft reports the work-in-progress / draft state uniformly across platforms
	// (GitHub pr.draft, Gitea draft, GitLab/TencentCode work_in_progress).
	Draft       bool      `json:"draft"`
	Author      *CRUser   `json:"author"`
	Assignees   []*CRUser `json:"assignees,omitempty"`
	Reviewers   []*CRUser `json:"reviewers"`
	Labels      []string  `json:"labels"`
	MergeStatus string    `json:"merge_status"`
	WebURL      string    `json:"web_url"`
	CreatedAt   time.Time `json:"created_at"`
	UpdatedAt   time.Time `json:"updated_at"`
}

ChangeRequest represents a pull request or merge request. Number is the platform's change-request identifier as a string (numeric on every current platform), mirroring Issue.Number.

type ChangeRequestManager

type ChangeRequestManager interface {
	CreateCR(ctx context.Context, opts CreateCROptions) (*ChangeRequest, error)
	GetCR(ctx context.Context, owner, repo, number string) (*ChangeRequest, error)
	// ListCRs lists change requests. total is nil when the platform
	// provided no authoritative total for the page (an unknown count is
	// reported as unknown, never as the page length); a non-nil total is
	// the platform's own metadata (exact or header-derived).
	ListCRs(ctx context.Context, opts ListCROptions) ([]*ChangeRequest, *int, error)
	MergeCR(ctx context.Context, owner, repo, number string, opts MergeCROptions) (*ChangeRequest, error)
	CloseCR(ctx context.Context, owner, repo, number string) (*ChangeRequest, error)
	ReopenCR(ctx context.Context, owner, repo, number string) (*ChangeRequest, error)
	UpdateCR(ctx context.Context, owner, repo, number string, opts UpdateCROptions) (*ChangeRequest, error)
	UpdateCRLabels(ctx context.Context, owner, repo, number string, labels []string) error
	ListCRComments(ctx context.Context, owner, repo, number string) ([]*CRComment, error)
	ListCRCommits(ctx context.Context, owner, repo, number string) ([]*CRCommit, error)
}

ChangeRequestManager handles pull request / merge request lifecycle. Change request numbers are strings (same addressing scheme as IssueManager); numeric platforms parse with strconv and fail with a wrapped "invalid pull request number" error.

type ChangedFile

type ChangedFile struct {
	OldPath   string `json:"old_path"`
	NewPath   string `json:"new_path"`
	Diff      string `json:"diff"`
	Additions int    `json:"additions"`
	Deletions int    `json:"deletions"`
	IsNew     bool   `json:"new_file"`
	IsDeleted bool   `json:"deleted_file"`
	IsRenamed bool   `json:"renamed_file"`
	IsBinary  bool   `json:"binary"`
}

ChangedFile represents a file changed in a change request.

type Collaborator

type Collaborator struct {
	ID         int64   `json:"id"`
	Username   string  `json:"username"`
	Permission string  `json:"permission,omitempty"`
	User       *CRUser `json:"user,omitempty"`
}

Collaborator represents a repository collaborator.

type CollaboratorManager

type CollaboratorManager interface {
	// ListCollaborators returns all collaborators on a repository.
	ListCollaborators(ctx context.Context, owner, repo string) ([]*Collaborator, error)
	// AddCollaborator adds a user as a collaborator. Permission is
	// platform-specific ("read", "write", "admin"; empty = default).
	AddCollaborator(ctx context.Context, owner, repo, username string, opts AddCollaboratorOptions) error
	// RemoveCollaborator removes a user from the repository.
	RemoveCollaborator(ctx context.Context, owner, repo, username string) error
}

CollaboratorManager provides repository collaborator management. It is an optional capability interface: consumers should gate on Provider.Capabilities().Collaborators (or type-assert) before use.

Platform support: GitHub, GitCode, GitLab, Gitea, Forgejo, Gitee, TencentCode. GitLab-family platforms (GitLab, TencentCode) back this with the project-members API: mutations resolve the username to a numeric user ID first (TTL-cached), and the read/write/admin permission vocabulary maps onto platform access levels (registered mappings).

type CombinedCommitStatus

type CombinedCommitStatus struct {
	State CommitStatusState `json:"state"`
	// Contexts maps each contributing context to its latest state.
	Contexts map[string]CommitStatusState `json:"contexts,omitempty"`
}

CombinedCommitStatus is the fold of a commit's statuses with a snapshot of the per-context states it was derived from.

func (CombinedCommitStatus) String

func (c CombinedCommitStatus) String() string

type CommitInfo

type CommitInfo struct {
	SHA       string    `json:"sha"`
	Message   string    `json:"message"`
	Author    *CRUser   `json:"author"`
	Committer *CRUser   `json:"committer"`
	CreatedAt time.Time `json:"created_at"`
	Additions int       `json:"additions"`
	Deletions int       `json:"deletions"`
}

CommitInfo represents a commit.

type CommitManager

type CommitManager interface {
	GetCommit(ctx context.Context, owner, repo, sha string) (*CommitInfo, error)
	ListCommits(ctx context.Context, owner, repo string, opts ListCommitsOptions) ([]*CommitInfo, error)
	CompareCommits(ctx context.Context, owner, repo, base, head string) (*CompareResult, error)
}

CommitManager handles commit operations.

Commit statuses are NOT part of CommitManager: they are a CI reporting concern that not every platform exposes (Gitee's public REST API has no commit-status endpoint). See the optional CommitStatusManager capability interface and CapabilitySet.CommitStatuses.

type CommitStatus

type CommitStatus struct {
	State       CommitStatusState `json:"state"`
	Context     string            `json:"context"`
	Description string            `json:"description,omitempty"`
	TargetURL   string            `json:"target_url,omitempty"`
}

CommitStatus is one status reported on a commit, unified across platforms. State is normalized via NormalizeCommitStatusState.

func LatestCommitStatuses

func LatestCommitStatuses(statuses []CommitStatus) []CommitStatus

LatestCommitStatuses collapses a commit's status history to one status per context, keeping the FIRST occurrence of each context. All platform list endpoints return statuses newest-first (GitHub documents reverse chronological order; Gitea/GitLab/Gitee/GitCode/Forgejo/Tencent Code sort descending by creation), so first-seen is the most recent report of that context — the same entry GitHub's combined status would use. CI re-runs append history instead of replacing it, so folding the raw list would let a stale failure (or pending) outvote the latest result.

type CommitStatusManager

type CommitStatusManager interface {
	CreateCommitStatus(ctx context.Context, owner, repo, sha string, opts CommitStatusOptions) error
	// ListCommitStatuses returns the statuses reported on a commit,
	// newest first (the platforms' native order). State is normalized
	// into the CommitStatusState vocabulary by the backend.
	ListCommitStatuses(ctx context.Context, owner, repo, sha string) ([]CommitStatus, error)
}

CommitStatusManager reports CI statuses on commits. It is an optional capability interface: consumers should gate on Provider.Capabilities().CommitStatuses (or type-assert) before use.

It is deliberately separate from CommitManager: commit statuses are a CI reporting concern that not every platform exposes (Gitee's public REST API has no commit-status endpoint), so absence is expressed by not declaring the capability instead of stubbing the method.

type CommitStatusOptions

type CommitStatusOptions struct {
	State       string `json:"state"`
	Context     string `json:"context"`
	Description string `json:"description,omitempty"`
	TargetURL   string `json:"target_url,omitempty"`
}

CommitStatusOptions contains options for creating a commit status.

type CommitStatusState

type CommitStatusState string

CommitStatusState is the unified state vocabulary for commit statuses. It covers the union of the platforms' verbs: GitHub (error/failure/pending/success), GitLab (pending/running/success/ failed/canceled and pipeline pre-states), Gitea/Forgejo (pending/success/error/failure/warning), and the GitHub-shaped verbs spoken by GitCode, Gitee (via check-run conclusions) and Tencent Code.

const (
	// CommitStatusPending is the "not finished yet" state: the check is
	// queued or running. This is the only non-terminal state.
	CommitStatusPending CommitStatusState = "pending"
	// CommitStatusRunning is an alias of CommitStatusPending kept as a
	// distinct constant so callers can surface GitLab-style running
	// states verbatim; CombineCommitStatusStates treats it as pending.
	CommitStatusRunning CommitStatusState = "running"
	// CommitStatusSuccess is the terminal "passed" state.
	CommitStatusSuccess CommitStatusState = "success"
	// CommitStatusFailure is the terminal "failed" state.
	CommitStatusFailure CommitStatusState = "failure"
	// CommitStatusError is the terminal "errored before/without a run"
	// state. It is terminal like CommitStatusFailure but distinct so
	// infrastructure breakage stays tellable from test breakage.
	CommitStatusError CommitStatusState = "error"
	// CommitStatusCanceled is the terminal "canceled" state (GitLab).
	CommitStatusCanceled CommitStatusState = "canceled"
)

func CombineCommitStatusStates

func CombineCommitStatusStates(states []CommitStatusState) CommitStatusState

CombineCommitStatusStates folds per-context states into the single combined state of a commit, using the same precedence as GitHub's combined status: any error wins, then failure, then canceled; any pending/running keeps the commit pending; only an all-success set is success. An empty set is pending — a commit with no reported statuses has nothing to gate on yet.

func NormalizeCommitStatusState

func NormalizeCommitStatusState(state string) CommitStatusState

NormalizeCommitStatusState maps a platform's raw status verb onto the unified CommitStatusState vocabulary. Unknown verbs normalize to CommitStatusPending so that unrecognised in-flight states keep callers waiting instead of concluding early; platform backends map their own verbs through this helper before returning CommitStatus values.

Gitea/Forgejo "warning" (build passed with warnings) normalizes to success: it is a terminal passing outcome, and merge gates treat it as such on the platform itself.

func WaitForCommitStatus

func WaitForCommitStatus(ctx context.Context, p Provider, owner, repo, sha string, opts WaitOptions) (CommitStatusState, error)

WaitForCommitStatus polls ListCommitStatuses until the commit's combined state is terminal (success/failure/error/canceled) and returns it. It is the CI-gate primitive for automation and agent flows: create your status (or push a commit), wait, then decide.

The provider must implement CommitStatusManager; otherwise the error wraps ErrNotImplemented. Context cancellation is honored between and during polls. A combined state of pending/running past the timeout fails with ErrWaitTimedOut (so a red build is never confused with "gave up waiting"): distinguish them via errors.Is.

States are combined with CombineCommitStatusStates semantics — any error beats failure beats canceled; any pending/running keeps waiting; an all-success set succeeds. With WaitOptions.Contexts set, only the named contexts are folded; contexts that have not reported yet are simply absent, so the wait continues until each named context lands a terminal state.

func (CommitStatusState) Terminal

func (s CommitStatusState) Terminal() bool

Terminal reports whether the state is a final outcome: success, failure, error, or canceled — anything but the pending/running family and the empty state.

type CompareResult

type CompareResult struct {
	Commits      []*CommitInfo  `json:"commits"`
	Files        []*ChangedFile `json:"files"`
	TotalCommits int            `json:"total_commits"`
	AheadBy      int            `json:"ahead_by"`
	BehindBy     int            `json:"behind_by"`
}

CompareResult represents the result of comparing two commits.

type Config

type Config struct {
	Platform Platform
	BaseURL  string
	Token    string
	SkipTLS  bool

	// TokenSource, when non-nil, supplies the access token on every
	// request through the transport layer and takes precedence over Token.
	// Use it for credentials that rotate or expire (GitHub App installation
	// tokens, OAuth flows); see the TokenSource interface. Static tokens
	// keep using Token.
	TokenSource TokenSource

	// ConditionalRequests enables HTTP conditional requests for GETs: the
	// transport caches responses keyed by their ETag, sends If-None-Match
	// on re-reads, and replays the cached body when the platform answers
	// 304. On platforms that meter 304s favorably (GitHub) this raises
	// sustainable polling rates for wait-for-CI style workloads. Off by
	// default; see transport.ETagCache for the exact semantics.
	ConditionalRequests bool

	// TokenStyle overrides the default authentication header style.
	// Supported values: "private" (PRIVATE-TOKEN, default for GitLab),
	// "bearer" (Authorization: Bearer). Empty string uses platform default.
	TokenStyle string

	// Logger for provider operations. Defaults to a no-op logger.
	Logger Logger
	// RetryConfig for automatic retry on transient failures. nil means no retry.
	RetryConfig *RetryConfig
	// Hooks for request/response lifecycle interception.
	Hooks *Hooks
}

Config holds the configuration for creating a Provider.

type Contributor

type Contributor struct {
	Username      string `json:"username"`
	Contributions int    `json:"contributions"`
}

Contributor represents a repository contributor.

type CreateBranchProtectionOptions

type CreateBranchProtectionOptions struct {
	BranchName               string `json:"branch_name"`
	RequiredApprovingReviews int    `json:"required_approving_reviews,omitempty"`
	RequiredStatusChecks     bool   `json:"required_status_checks,omitempty"`
	AllowForcePushes         bool   `json:"allow_force_pushes,omitempty"`
	AllowDeletions           bool   `json:"allow_deletions,omitempty"`
}

CreateBranchProtectionOptions contains options for creating a branch protection rule.

type CreateCROptions

type CreateCROptions struct {
	Owner              string   `json:"owner"`
	Repo               string   `json:"repo"`
	Title              string   `json:"title"`
	Description        string   `json:"description,omitempty"`
	SourceBranch       string   `json:"source_branch"`
	TargetBranch       string   `json:"target_branch"`
	Labels             []string `json:"labels,omitempty"`
	RemoveSourceBranch bool     `json:"remove_source_branch,omitempty"`
}

CreateCROptions contains options for creating a change request.

type CreateIssueOptions

type CreateIssueOptions struct {
	Owner     string   `json:"owner"`
	Repo      string   `json:"repo"`
	Title     string   `json:"title"`
	Body      string   `json:"body,omitempty"`
	Assignees []string `json:"assignees,omitempty"`
	Labels    []string `json:"labels,omitempty"`
	Milestone string   `json:"milestone,omitempty"` // milestone number/ID as a string; "" = do not set
}

CreateIssueOptions contains options for creating an issue.

type CreateLabelOptions

type CreateLabelOptions struct {
	Name        string `json:"name"`
	Color       string `json:"color"`
	Description string `json:"description,omitempty"`
}

CreateLabelOptions contains options for creating a repository label. Color uses the canonical 6-digit hex form without '#' (e.g. "ff0000").

type CreateMigrationOptions added in v0.72.0

type CreateMigrationOptions struct {
	// LockRepositories 迁移期间锁定仓库(防止导出过程中数据变动)。
	LockRepositories bool `json:"lock_repositories"`
	// ExcludeMetadata 跳过 issues/PR 等元数据、只导出仓库内容。
	ExcludeMetadata bool `json:"exclude_metadata"`
}

CreateMigrationOptions 发起归档迁移的选项。json tag 与 GitHub 迁移 API 请求体字段一致,便于后端直接序列化上送。

type CreateMilestoneOptions

type CreateMilestoneOptions struct {
	Title       string     `json:"title"`
	Description string     `json:"description,omitempty"`
	DueOn       *time.Time `json:"due_on,omitempty"`
}

CreateMilestoneOptions contains options for creating a repository milestone.

type CreateReleaseOptions

type CreateReleaseOptions struct {
	TagName    string `json:"tag_name"`
	Target     string `json:"target,omitempty"`
	Title      string `json:"title"`
	Body       string `json:"body,omitempty"`
	Draft      bool   `json:"draft,omitempty"`
	Prerelease bool   `json:"prerelease,omitempty"`
}

CreateReleaseOptions contains options for creating a release.

type CreateRepoOptions

type CreateRepoOptions struct {
	Name          string `json:"name"`
	Description   string `json:"description,omitempty"`
	Private       bool   `json:"private,omitempty"`
	AutoInit      bool   `json:"auto_init,omitempty"`
	DefaultBranch string `json:"default_branch,omitempty"`
}

CreateRepoOptions contains options for creating a repository.

type CreateReviewOptions

type CreateReviewOptions struct {
	CommitID string          `json:"commit_id,omitempty"`
	Event    string          `json:"event,omitempty"`
	Body     string          `json:"body,omitempty"`
	Comments []ReviewComment `json:"comments,omitempty"`
}

CreateReviewOptions contains options for creating a review.

type CreateWebhookOptions

type CreateWebhookOptions struct {
	Owner  string   `json:"owner"`
	Repo   string   `json:"repo"`
	URL    string   `json:"url"`
	Secret string   `json:"secret,omitempty"`
	Events []string `json:"events,omitempty"`
}

CreateWebhookOptions contains options for creating a webhook.

type DeployKey

type DeployKey struct {
	ID       int64  `json:"id"`
	Title    string `json:"title"`
	Key      string `json:"key"`
	ReadOnly bool   `json:"read_only"`
}

DeployKey represents a deploy key on a repository.

type DeploymentKeyManager

type DeploymentKeyManager interface {
	// ListDeployKeys returns all deploy keys for a repository.
	ListDeployKeys(ctx context.Context, owner, repo string) ([]*DeployKey, error)
	// AddDeployKey adds a new deploy key to the repository.
	AddDeployKey(ctx context.Context, owner, repo string, opts AddDeployKeyOptions) (*DeployKey, error)
	// DeleteDeployKey removes a deploy key by its platform ID.
	DeleteDeployKey(ctx context.Context, owner, repo string, keyID int64) error
}

DeploymentKeyManager provides deploy key CRUD for CI/CD. It is an optional capability interface: consumers should gate on Provider.Capabilities().DeployKeys (or type-assert) before use.

Platform support: GitHub, GitCode, GitLab, Gitea, Forgejo. Gitee and TencentCode have no deploy key API.

type DetectResult

type DetectResult struct {
	Platform Platform
	Owner    string
	Repo     string
	BaseURL  string
}

DetectResult holds the platform, owner, repo, and base API URL extracted from a git remote URL by DetectPlatform.

Owner/Repo are split as "first path segment = Owner, remainder = Repo". For GitLab repositories nested in subgroups the full subgroup path stays part of Repo: "https://gitlab.com/group/sub/repo" yields Owner="group" and Repo="sub/repo". Callers must therefore treat Repo as a path (it may contain "/"), not a bare repository name.

func DetectPlatform

func DetectPlatform(remoteURL string) (*DetectResult, error)

DetectPlatform parses a git remote URL (HTTPS, SSH, or ssh://) and returns the detected platform, owner, repo name, and base API URL. Returns ErrPlatformNotSupported for unrecognized hosts; use NewProvider with explicit Config for self-hosted instances not in the known-host list.

type DiffManager

type DiffManager interface {
	GetCRDiff(ctx context.Context, owner, repo, number string) (*MergeDiff, error)
	GetCRFiles(ctx context.Context, owner, repo, number string) ([]*ChangedFile, error)
	CreateNote(ctx context.Context, owner, repo, number, body string) (string, error)
	DeleteNote(ctx context.Context, owner, repo, number string, noteID string) error
	CreateDiscussion(ctx context.Context, owner, repo, number string, opts DiscussionOptions) (string, error)
}

DiffManager handles diff and discussion operations. Review operations (CreateReview and friends) live on the optional ReviewManager capability interface; DiffManager itself carries five methods. Change request numbers are strings (same addressing scheme as IssueManager); numeric platforms parse with strconv and fail with a wrapped "invalid pull request number" error.

type DiscussionOptions

type DiscussionOptions struct {
	Body         string `json:"body"`
	FilePath     string `json:"file_path,omitempty"`
	NewLine      int    `json:"new_line,omitempty"`
	OldLine      int    `json:"old_line,omitempty"`
	StartNewLine int    `json:"start_new_line,omitempty"`
	BaseSHA      string `json:"base_sha,omitempty"`
	StartSHA     string `json:"start_sha,omitempty"`
	HeadSHA      string `json:"head_sha,omitempty"`
}

DiscussionOptions contains options for creating a discussion comment.

type Divergence

type Divergence struct {
	Capability string
	Method     string
	Field      string
	Kind       DivergenceKind
	Reason     string
}

Divergence is one registered entry of a backend's divergence ledger. Capability and Method carry the provider interface and method names; Field names the affected option/result field for ignore and mapping entries (empty when the divergence is method-scoped). Reason is a one-sentence explanation surfaced in docs/divergence-ledger.md.

Backends expose their ledger via a package-level Divergences function and the Provider.Divergences method; the ledger is the machine-readable successor of the former "(spec §4.6)" comment registrations.

func FindByMethod

func FindByMethod(divs []Divergence, method string) []Divergence

FindByMethod returns the ledger entries registered for method.

type DivergenceKind

type DivergenceKind string

DivergenceKind classifies how a backend's behavior departs from the unified provider semantics for a given method.

const (
	// DivergenceStub marks a method the platform cannot serve at all: the
	// call returns an error wrapping ErrNotImplemented and touches no wire.
	DivergenceStub DivergenceKind = "stub"
	// DivergenceIgnore marks a field or parameter that is silently dropped:
	// the call succeeds but the ignored input has no effect.
	DivergenceIgnore DivergenceKind = "ignore"
	// DivergenceMapping marks a semantic mapping: the call succeeds and
	// returns the closest platform equivalent, an approximation of the
	// unified semantics.
	DivergenceMapping DivergenceKind = "mapping"
	// DivergenceDetour marks an implementation detour: the method bypasses
	// the platform's third-party SDK and drives the raw transport client.
	// Behavior is unchanged; the entry exists for maintainers.
	DivergenceDetour DivergenceKind = "detour"
)

type EnsureAction added in v0.72.0

type EnsureAction string

EnsureAction reports what an Ensure* call did to converge the resource onto its desired state.

const (
	// EnsureCreated: the resource did not exist and was created.
	EnsureCreated EnsureAction = "created"
	// EnsureUpdated: the resource existed with different content and was
	// updated (for webhooks and deploy keys: replaced, see below).
	EnsureUpdated EnsureAction = "updated"
	// EnsureUnchanged: the resource already matched the desired state.
	EnsureUnchanged EnsureAction = "unchanged"
)

Ensure* outcomes.

type EventRepo

type EventRepo struct {
	ID       int64  `json:"id"`
	FullName string `json:"full_name"`
	Owner    string `json:"owner"`
	Name     string `json:"name"`
}

EventRepo represents the repository in a webhook event.

func BuildEventRepo

func BuildEventRepo(fullName string) *EventRepo

BuildEventRepo creates an EventRepo from a full name string.

type FileDeleteOptions

type FileDeleteOptions struct {
	Path    string `json:"path"`
	Message string `json:"message"`
	Branch  string `json:"branch,omitempty"`
	SHA     string `json:"sha,omitempty"`
	Author  string `json:"author,omitempty"`
	Email   string `json:"email,omitempty"`
}

FileDeleteOptions contains options for deleting a file.

type FileManager

type FileManager interface {
	GetFileContent(ctx context.Context, owner, repo, path, ref string) (string, error)
	CreateFile(ctx context.Context, owner, repo string, opts FileOptions) (*FileResult, error)
	UpdateFile(ctx context.Context, owner, repo string, opts FileOptions) (*FileResult, error)
	DeleteFile(ctx context.Context, owner, repo string, opts FileDeleteOptions) (*FileResult, error)
}

FileManager handles file CRUD operations on repositories.

type FileOptions

type FileOptions struct {
	Path    string `json:"path"`
	Content string `json:"content"`
	Message string `json:"message"`
	Branch  string `json:"branch,omitempty"`
	SHA     string `json:"sha,omitempty"`
	Author  string `json:"author,omitempty"`
	Email   string `json:"email,omitempty"`
}

FileOptions contains options for creating or updating a file.

type FileResult

type FileResult struct {
	SHA       string `json:"sha,omitempty"`
	CommitSHA string `json:"commit_sha,omitempty"`
}

FileResult is the result of a file operation.

type ForgejoWebhookValidator added in v0.72.0

type ForgejoWebhookValidator struct{}

ForgejoWebhookValidator validates Forgejo's HMAC-SHA256 webhook signature. Current Forgejo sends X-Forgejo-Signature; releases forked from older Gitea send X-Gitea-Signature, so both are accepted — whichever header is present is used (the signature format itself is identical raw-hex HMAC-SHA256).

func (ForgejoWebhookValidator) Name added in v0.72.0

Name implements WebhookValidator.

func (ForgejoWebhookValidator) Validate added in v0.72.0

func (ForgejoWebhookValidator) Validate(r *http.Request, body []byte, secret string) error

Validate implements WebhookValidator.

type ForkRepoOptions

type ForkRepoOptions struct {
	Organization string `json:"organization,omitempty"`
	Name         string `json:"name,omitempty"`
}

ForkRepoOptions contains options for forking a repository.

type Gist added in v0.72.0

type Gist struct {
	ID          string              `json:"id"`
	Description string              `json:"description"`
	Public      bool                `json:"public"`
	HTMLURL     string              `json:"html_url"`
	CreatedAt   time.Time           `json:"created_at"`
	UpdatedAt   time.Time           `json:"updated_at"`
	Files       map[string]GistFile `json:"files"`
}

Gist GitHub Gist 元数据。json tag 与 GitHub API 对齐,便于消费方直接 序列化落盘备份(下游 BackupGists 即整体 json.MarshalIndent)。

type GistFile added in v0.72.0

type GistFile struct {
	Filename string `json:"filename"`
	Language string `json:"language"`
	RawURL   string `json:"raw_url"`
	Size     int64  `json:"size"`
	Content  string `json:"content"`
}

GistFile 单个 gist 文件的元数据与内容。json tag 与 GitHub /gists 响应对齐, 与消费方(git-sync-service githubapi.Gist 内嵌结构)字段保持兼容。

type GistManager added in v0.72.0

type GistManager interface {
	// ListMyGists 分页列出 token 用户可见的 gist(GET /gists:已认证调用
	// 返回当前用户的 gist 列表)。page/perPage 语义与其他分页方法一致,
	// 非法值经 NormalizePageOpts 归一。
	ListMyGists(ctx context.Context, page, perPage int) ([]*Gist, error)
}

GistManager 提供认证用户自己的 gist 列表。可选能力接口:使用前先看 Provider.Capabilities().Gists(或类型断言)。目前只读——下游备份场景 只需要分页列出,创建/修改 gist 不在统一接口范围内。

type GitCodeWebhookValidator added in v0.72.0

type GitCodeWebhookValidator struct{}

GitCodeWebhookValidator accepts GitCode's two webhook verification modes: an HMAC-SHA256 body signature (X-GitCode-Signature, falling back to the gitea-inherited X-Gitea-Signature) or the documented static password in X-GitCode-Token. Whichever header is present is used.

func (GitCodeWebhookValidator) Name added in v0.72.0

Name implements WebhookValidator.

func (GitCodeWebhookValidator) Validate added in v0.72.0

func (GitCodeWebhookValidator) Validate(r *http.Request, body []byte, secret string) error

Validate implements WebhookValidator.

type GiteeWebhookValidator

type GiteeWebhookValidator struct {
	// MaxAge bounds |now - X-Gitee-Timestamp| in sign mode. Zero means
	// DefaultGiteeSignMaxAge; a negative value disables the freshness
	// check (not recommended — it enables indefinite replay).
	MaxAge time.Duration
	// AllowPasswordMode also accepts the plain-password form when
	// X-Gitee-Timestamp is absent. Enable only when the webhook is known
	// to be configured in password mode.
	AllowPasswordMode bool
}

GiteeWebhookValidator verifies Gitee webhooks. Gitee offers two security modes and both ride the X-Gitee-Token header:

  • 签名密钥 (sign) mode: X-Gitee-Timestamp carries the send time in unix millis and X-Gitee-Token carries Base64(HMAC-SHA256(key=secret, msg=timestamp+"\n"+secret)). This is the default mode this validator checks, including a timestamp freshness bound (replay protection).
  • 密码 (password) mode: X-Gitee-Token carries the configured password verbatim, X-Gitee-Timestamp is absent, and the signature cannot be computed. Opt in with AllowPasswordMode; the token is then compared against the secret in constant time.

The webhook body plays no part in either scheme (Gitee signs the timestamp, not the payload) — transport-level integrity is delegated to TLS.

func (GiteeWebhookValidator) Name

Name implements WebhookValidator.

func (GiteeWebhookValidator) Validate

func (v GiteeWebhookValidator) Validate(r *http.Request, body []byte, secret string) error

Validate implements WebhookValidator.

type HMACSHA256Validator

type HMACSHA256Validator struct {
	Header string
}

HMACSHA256Validator verifies a "Header: sha256=<hex>" style signature using HMAC-SHA256 over the request body. The expected header is configurable so it works for GitHub (X-Hub-Signature-256), Gitea (X-Gitea-Signature), Gitee (X-Gitee-Token) and others.

func (HMACSHA256Validator) Name

func (HMACSHA256Validator) Name() string

Name implements WebhookValidator.

func (HMACSHA256Validator) Validate

func (h HMACSHA256Validator) Validate(r *http.Request, body []byte, secret string) error

Validate implements WebhookValidator. The signature must be exactly "sha256=<hex>" or just "<hex>". Comparison is done in constant time.

type HmacValidator

type HmacValidator struct {
	Header    string
	Algorithm string // "sha1", "sha256", "sha512"
	Prefix    string // expected signature prefix ("sha256=")
}

HmacValidator is a lower-level helper that supports arbitrary hash algorithms. It is intended for use by platform implementations that need something other than SHA-256.

func (*HmacValidator) Name

func (h *HmacValidator) Name() string

Name implements WebhookValidator.

func (*HmacValidator) Validate

func (h *HmacValidator) Validate(r *http.Request, body []byte, secret string) error

Validate implements WebhookValidator.

type Hooks

type Hooks struct {
	Request  []RequestHook
	Response []ResponseHook
}

Hooks holds request and response lifecycle hooks. These are mapped into transport.Hooks by each backend's constructor; direct callers should use the Hooks struct to register hooks via provider.Config.

func (*Hooks) AddRequestHook

func (h *Hooks) AddRequestHook(hook RequestHook)

AddRequestHook appends a request hook.

func (*Hooks) AddResponseHook

func (h *Hooks) AddResponseHook(hook ResponseHook)

AddResponseHook appends a response hook.

type Issue

type Issue struct {
	ID        int64         `json:"id"`
	Number    string        `json:"number"`
	Title     string        `json:"title"`
	Body      string        `json:"body"`
	State     IssueState    `json:"state"`
	Author    *CRUser       `json:"author,omitempty"`
	Labels    []string      `json:"labels,omitempty"`
	Assignees []string      `json:"assignees,omitempty"`
	Milestone *MilestoneRef `json:"milestone,omitempty"`
	WebURL    string        `json:"web_url,omitempty"`
	CreatedAt time.Time     `json:"created_at"`
	UpdatedAt time.Time     `json:"updated_at"`
	ClosedAt  *time.Time    `json:"closed_at,omitempty"`
	// IsPullRequest 标记这条"issue"其实是 PR。
	//
	// **GitHub 的 issue 列表端点会把 PR 一起返回**(同一条记录既是 issue 也是 PR),
	// 不标记的话消费方没法把两者分开 ⇒ 同一个 PR 既进 issues 快照又进
	// pull_requests 快照,而且**占掉 issue 的数量预算**(默认 500)
	// ⇒ PR 多的仓里真实 issue 可能整批落在预算之外,快照缺项且无任何告警。
	//
	// Gitea/Forgejo 家族的列表端点有 type=issues 过滤(本来就不含 PR),
	// GitLab 的 issues 端点也不含 MR ⇒ 只有 GitHub 需要这个标记。
	IsPullRequest bool `json:"is_pull_request,omitempty"`
}

Issue represents an issue on a platform. Number is the platform's issue identifier as a string (numeric on every current platform except Gitee, whose identifiers are alphanumeric).

type IssueComment

type IssueComment struct {
	ID        int64     `json:"id"`
	Body      string    `json:"body"`
	Author    *CRUser   `json:"author,omitempty"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

IssueComment represents a comment on an issue.

func (*IssueComment) Mentions

func (c *IssueComment) Mentions() []string

Mentions returns the deduplicated @usernames found in the comment body.

type IssueLabel

type IssueLabel struct {
	ID    int64  `json:"id"`
	Name  string `json:"name"`
	Color string `json:"color,omitempty"`
}

IssueLabel represents a label on a repository.

type IssueManager

type IssueManager interface {
	ListIssues(ctx context.Context, opts ListIssuesOptions) ([]*Issue, int, error)
	GetIssue(ctx context.Context, owner, repo, number string) (*Issue, error)
	CreateIssue(ctx context.Context, opts CreateIssueOptions) (*Issue, error)
	UpdateIssue(ctx context.Context, owner, repo, number string, opts UpdateIssueOptions) (*Issue, error)
	CloseIssue(ctx context.Context, owner, repo, number string) (*Issue, error)
	ReopenIssue(ctx context.Context, owner, repo, number string) (*Issue, error)
	// ListIssueComments returns the issue's comments in full, exhausting
	// the platform's pagination — the result is the complete comment list,
	// not a single page, in the platform's default order.
	ListIssueComments(ctx context.Context, owner, repo, number string) ([]*IssueComment, error)
	CreateIssueComment(ctx context.Context, owner, repo, number, body string) (*IssueComment, error)
	// UpdateIssueComment replaces a comment's body wholesale: every
	// platform's edit surface is a body-only PATCH/PUT with no
	// partial-update semantics, mirroring CreateIssueComment. The platform
	// enforces authorship — only the comment's author (typically the token
	// identity, e.g. a review bot updating its own comment) may edit — so
	// the call fails for anyone else's comment. commentID is the
	// IssueComment.ID from CreateIssueComment or ListIssueComments. number
	// is ignored on platforms whose edit endpoint addresses the comment
	// directly (GitHub, Gitea, Forgejo, GitCode, Gitee); GitLab and Tencent
	// 工蜂 route through the issue, so it must carry that issue's number.
	UpdateIssueComment(ctx context.Context, owner, repo, number string, commentID int64, body string) (*IssueComment, error)
	ListIssueLabels(ctx context.Context, owner, repo string) ([]*IssueLabel, error)
	AddIssueLabels(ctx context.Context, owner, repo, number string, labels []string) error
	RemoveIssueLabel(ctx context.Context, owner, repo, number, name string) error
}

IssueManager provides issue CRUD, comments, and label management. Issue numbers are strings: every platform address is representable as a string, and Gitee natively uses alphanumeric identifiers (e.g. "IAINVA"). Backends on numeric platforms parse with strconv.Atoi and fail with a wrapped "invalid issue number" error.

type IssueState

type IssueState string

IssueState represents the state of an issue.

const (
	IssueStateOpen   IssueState = "open"
	IssueStateClosed IssueState = "closed"
)

type JobLog added in v0.67.0

type JobLog struct {
	Name    string `json:"name"`
	Stage   string `json:"stage"`
	WebURL  string `json:"web_url,omitempty"`
	Trace   string `json:"trace"` // 失败日志尾部(后端截断,量级 ~20KB)
	Truncat bool   `json:"truncated"`
}

JobLog is one failed CI job's identifying metadata plus the tail of its execution log, sized for LLM consumption rather than archival.

type Label

type Label struct {
	ID          int64  `json:"id"`
	Name        string `json:"name"`
	Color       string `json:"color,omitempty"`
	Description string `json:"description,omitempty"`
}

Label represents a repository label. Color is canonicalized to 6-digit hex without a leading '#' (e.g. "ff0000"); backends add the '#' when a platform requires it (GitLab, Gitea, Forgejo) and strip it on the way in.

type LabelManager

type LabelManager interface {
	// ListLabels lists the repository's labels.
	ListLabels(ctx context.Context, owner, repo string, opts ListLabelsOptions) ([]*Label, error)
	// CreateLabel creates a repository label.
	CreateLabel(ctx context.Context, owner, repo string, opts CreateLabelOptions) (*Label, error)
	// UpdateLabel updates the label with the given name. Nil fields in opts
	// are left unchanged.
	UpdateLabel(ctx context.Context, owner, repo, name string, opts UpdateLabelOptions) (*Label, error)
	// DeleteLabel deletes the label with the given name.
	DeleteLabel(ctx context.Context, owner, repo, name string) error
}

LabelManager provides repository-level label CRUD. It is an optional capability interface: consumers should gate on Provider.Capabilities() (or type-assert) before use. Labels are addressed by name; backends whose platform API addresses labels by numeric ID (GitLab, Gitea, Forgejo) resolve the name internally. Such backends scan labels with server-side pagination (100 per page, bounded to 50 pages); beyond that bound a label may be reported as not found by UpdateLabel/DeleteLabel even though it exists.

The issue-scoped operations (ListIssueLabels, AddIssueLabels, RemoveIssueLabel) remain on IssueManager because they operate on an issue, not on the repository's label set.

type ListCROptions

type ListCROptions struct {
	Owner string `json:"owner"`
	Repo  string `json:"repo"`
	// State 过滤状态。**空 = 不过滤(全部)**。
	//
	// 各后端的原生词表不同(open/closed/merged 与 opened/closed)⇒ 平台层的
	// "空" 语义必须统一,否则同一个调用在有的平台只拿到 open、在别的平台拿到全部
	// —— 调用方无从察觉 closed/merged 的那部分**静默丢失**。
	// (GitHub / GitLab / 工蜂 本来就是"空=不过滤";Gitee / Gitea / Forgejo /
	//  GitCode 曾把空值映射成 open ⇒ 已对齐。)
	State        CRState `json:"state,omitempty"`
	SourceBranch string  `json:"source_branch,omitempty"`
	TargetBranch string  `json:"target_branch,omitempty"`
	Page         int     `json:"page,omitempty"`
	PerPage      int     `json:"per_page,omitempty"`
}

ListCROptions contains options for listing change requests.

type ListCommitsOptions

type ListCommitsOptions struct {
	Page    int    `json:"page,omitempty"`
	PerPage int    `json:"per_page,omitempty"`
	Branch  string `json:"branch,omitempty"`
	Since   string `json:"since,omitempty"` // RFC3339
	Until   string `json:"until,omitempty"` // RFC3339
}

ListCommitsOptions contains options for listing commits in a repository.

type ListIssuesOptions

type ListIssuesOptions struct {
	Owner    string     `json:"owner"`
	Repo     string     `json:"repo"`
	State    IssueState `json:"state,omitempty"`
	Assignee string     `json:"assignee,omitempty"`
	Labels   string     `json:"labels,omitempty"`
	Page     int        `json:"page,omitempty"`
	PerPage  int        `json:"per_page,omitempty"`
	// Sort 排序字段("created"|"updated"|"comments"),空 = 平台默认。
	//
	// **增量备份必须用它**:平台默认按 created 倒序 ⇒ "上次备份后被改动的
	// **老** issue"正好被挤在第一页之外,客户端再按 updated 过滤也捞不到
	// (捞到的永远是"最新创建"的那批)。
	//
	// 支持情况(逐后端登记在各自的 divergence 账本里):
	//   - GitHub:支持(sort + direction);
	//   - GitLab:支持(order_by=created_at|updated_at + sort=asc|desc);
	//   - Gitee / Gitea / Forgejo / GitCode / 工蜂:当前 SDK 不暴露(或词表无法确证)
	//     ⇒ 传入被忽略(调用方需自己兜底,见 core 的增量采集)。
	Sort string `json:"sort,omitempty"`
	// Direction 排序方向("asc"|"desc"),空 = 平台默认。
	Direction string `json:"direction,omitempty"`
}

ListIssuesOptions contains options for listing issues.

type ListLabelsOptions

type ListLabelsOptions struct {
	Page    int `json:"page,omitempty"`
	PerPage int `json:"per_page,omitempty"`
}

ListLabelsOptions contains options for listing repository labels.

type ListMilestonesOptions

type ListMilestonesOptions struct {
	State   string `json:"state,omitempty"`
	Page    int    `json:"page,omitempty"`
	PerPage int    `json:"per_page,omitempty"`
}

ListMilestonesOptions contains options for listing repository milestones. State filters by "open" or "closed"; an empty State lists whatever the platform defaults to (GitHub/Gitea/Forgejo/Gitee default to open, GitLab to all). Tencent Code ignores State entirely — gongfeng's list options expose pagination only, so all states are listed.

type ListNotificationsOptions

type ListNotificationsOptions struct {
	All     bool   `json:"all,omitempty"`   // include already-read notifications
	Since   string `json:"since,omitempty"` // RFC3339 timestamp
	Page    int    `json:"page,omitempty"`
	PerPage int    `json:"per_page,omitempty"`
}

ListNotificationsOptions contains options for listing notifications.

type ListRepoOptions

type ListRepoOptions struct {
	Owner   string `json:"owner,omitempty"`
	Page    int    `json:"page,omitempty"`
	PerPage int    `json:"per_page,omitempty"`
}

ListRepoOptions contains options for listing repositories on a platform.

type Logger

type Logger interface {
	Debug(msg string, keysAndValues ...any)
	Info(msg string, keysAndValues ...any)
	Warn(msg string, keysAndValues ...any)
	Error(msg string, keysAndValues ...any)
}

Logger is a minimal logging interface compatible with most Go logging libraries. Implementations include slog, zap, zerolog, logrus, etc.

func NewLogxLogger added in v0.85.0

func NewLogxLogger(component string) Logger

NewLogxLogger returns a Logger backed by the oekit/logx facade, for consumers who run the oekit observability stack. The logger is lazily resolved (logx.Delegating): it follows whatever implementation the consumer installs via logx.SetDefault / logx.UseSlog, even when that happens after NewProvider. component identifies this SDK in log output (the logx convention: service name belongs to the consumer, packages identify themselves via the component attribute).

p, err := provider.NewProvider(provider.Config{
    Platform: provider.PlatformGitHub,
    Token:    token,
    Logger:   provider.NewLogxLogger("git-platform"),
})

func NewNoopLogger

func NewNoopLogger() Logger

NewNoopLogger returns a Logger that discards all output.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager caches Provider instances keyed by configuration so repeated constructions of the same (platform, baseURL, token) reuse the underlying HTTP client pools instead of leaking a fresh one per call.

It automatically detects the platform from clone URLs and reuses existing Provider instances within the TTL window. The cache key is derived from platform + baseURL + a SHA-256 hash of the token, so different tokens map to different entries without leaking the token itself in logs or memory dumps.

The type is deliberately thin: the cache mechanics (TTL, LRU capacity eviction, counters) live in ttlCache and the background-cleanup lifecycle in janitor, leaving Manager exactly key derivation + factory call.

func NewManager

func NewManager(ttl time.Duration, opts ...ManagerOption) *Manager

NewManager creates a new Provider Manager with the given TTL. A TTL of 0 means providers never expire (until the process exits).

func (*Manager) Cleanup

func (m *Manager) Cleanup()

Cleanup removes expired entries from the cache. Safe to call manually; also invoked periodically by StartJanitor.

func (*Manager) Get

func (m *Manager) Get(cfg Config) (Provider, error)

Get returns a cached or newly created Provider for the given config. The cache key is platform + baseURL + hash(token), so the same (platform, baseURL) with a different token gets a distinct entry.

func (*Manager) GetByURL

func (m *Manager) GetByURL(cloneURL, token string) (Provider, error)

GetByURL detects the platform from the clone URL and returns a cached or newly created Provider.

func (*Manager) Len

func (m *Manager) Len() int

Len returns the number of cached Providers.

func (*Manager) Purge

func (m *Manager) Purge()

Purge removes all cached Providers.

func (*Manager) Remove

func (m *Manager) Remove(cfg Config)

Remove removes a cached Provider by config.

func (*Manager) ResetStats

func (m *Manager) ResetStats()

ResetStats zeroes the hit/miss/eviction counters. Cache entries are not affected.

func (*Manager) StartJanitor

func (m *Manager) StartJanitor(ctx context.Context, interval time.Duration)

StartJanitor launches a background goroutine that calls Cleanup every interval until ctx is cancelled or Stop is called. Calling StartJanitor more than once without an intervening Stop (or without the previous goroutine having exited) is a no-op. When the goroutine exits on its own — e.g. because ctx was cancelled — it resets the janitor state, so a subsequent StartJanitor with a fresh ctx launches a new goroutine instead of being mistaken for "already running".

func (*Manager) Stats

func (m *Manager) Stats() Stats

Stats returns a snapshot of cache counters. The counters continue to accumulate across calls; reset them with ResetStats.

Evictions 口径与本仓历史实现一致:容量淘汰与 TTL 过期回收**都计入** (上游 LocalCache 把两者分列为 Evictions / Expirations,此处按历史口径合并)。

func (*Manager) Stop

func (m *Manager) Stop()

Stop halts the background janitor and blocks until it has exited.

type ManagerOption

type ManagerOption func(*Manager)

ManagerOption configures a Manager at construction time.

func WithHasher

func WithHasher(h func(token string) string) ManagerOption

WithHasher overrides the default SHA-256 token hasher. Useful for tests that want deterministic, human-readable keys.

func WithMaxSize

func WithMaxSize(n int) ManagerOption

WithMaxSize caps the cache at n entries. When the cap is reached, the least recently used entry is evicted before a new one is inserted.

type MarkNotificationsOptions

type MarkNotificationsOptions struct {
	LastReadAt string `json:"last_read_at,omitempty"` // RFC3339; empty = mark all
}

MarkNotificationsOptions contains options for marking notifications as read.

type MergeCROptions

type MergeCROptions struct {
	MergeCommitMessage string `json:"merge_commit_message,omitempty"`
	Squash             bool   `json:"squash,omitempty"`
	RemoveSourceBranch bool   `json:"remove_source_branch,omitempty"`
}

MergeCROptions contains options for merging a change request.

type MergeDiff

type MergeDiff struct {
	Files    []*ChangedFile
	TotalAdd int
	TotalDel int
	RawDiff  string
}

MergeDiff represents the diff of a change request.

type MigrationInfo added in v0.72.0

type MigrationInfo struct {
	ID    int64  `json:"id"`
	State string `json:"state"`
	// ArchiveURL 归档下载地址(仅当 state 为 exported 时可用)。
	ArchiveURL string `json:"archive_url"`
}

MigrationInfo 迁移任务状态。json tag 与 GitHub 迁移 API 响应对齐。

type MigrationManager added in v0.72.0

type MigrationManager interface {
	// CreateMigration 发起迁移导出。org == "" 表示用户级迁移
	// (POST /user/migrations),非空表示组织级(POST /orgs/{org}/migrations)
	// ——同一语义贯穿本接口全部方法。
	CreateMigration(ctx context.Context, org string, opts CreateMigrationOptions) (*MigrationInfo, error)
	// GetMigration 查询迁移状态,org 语义与 CreateMigration 一致。
	GetMigration(ctx context.Context, org string, id int64) (*MigrationInfo, error)
}

MigrationManager 提供平台原生的「一键全量归档导出」(GitHub Migrations API)。可选能力接口:使用前先看 Provider.Capabilities().Migrations (或类型断言)。迁移是异步的:CreateMigration 立即返回任务 ID,消费方 轮询 GetMigration 直到 state 进入终态。

type Milestone

type Milestone struct {
	Number      string         `json:"number"`
	Title       string         `json:"title"`
	Description string         `json:"description"`
	State       MilestoneState `json:"state"`
	DueOn       *time.Time     `json:"due_on,omitempty"`
}

Milestone represents a repository milestone. Number carries the platform's milestone addressing identifier as a string — the same value MilestoneRef.Number uses and MilestoneManager methods accept: the milestone number on GitHub, the platform milestone ID on GitLab, Gitea, Forgejo, GitCode, and Tencent Code, and the milestone serial number on Gitee (see MilestoneManager for the per-platform truth).

type MilestoneManager

type MilestoneManager interface {
	// ListMilestones lists the repository's milestones.
	ListMilestones(ctx context.Context, owner, repo string, opts ListMilestonesOptions) ([]Milestone, error)
	// GetMilestone fetches the milestone with the given number.
	GetMilestone(ctx context.Context, owner, repo, number string) (*Milestone, error)
	// CreateMilestone creates a repository milestone.
	CreateMilestone(ctx context.Context, owner, repo string, opts CreateMilestoneOptions) (*Milestone, error)
	// UpdateMilestone updates the milestone with the given number. Nil
	// fields in opts are left unchanged.
	UpdateMilestone(ctx context.Context, owner, repo, number string, opts UpdateMilestoneOptions) (*Milestone, error)
	// DeleteMilestone deletes the milestone with the given number.
	DeleteMilestone(ctx context.Context, owner, repo, number string) error
}

MilestoneManager provides repository-level milestone CRUD. It is an optional capability interface: consumers should gate on Provider.Capabilities() (or type-assert) before use.

Milestones are addressed by a string `number`, but what that string carries is platform-specific — the same identifier MilestoneRef.Number and Milestone.Number expose:

  • GitHub: the milestone *number* (its per-repo serial number).
  • GitLab, Gitea, Forgejo, GitCode, Tencent Code: the platform milestone *ID* (the write endpoints take exactly that identifier, so per-platform round-trips hold).
  • Gitee: the milestone *serial number* (the "number" field of Gitee's milestone payload — the identifier Gitee's own issue and milestone write endpoints take; the SDK model exposes no id).

Values obtained from MilestoneRef.Number (issue payloads) or Milestone.Number (list/get results) round-trip back into these methods on the platform they came from.

type MilestoneRef

type MilestoneRef struct {
	Number string `json:"number"`
	Title  string `json:"title,omitempty"`
}

MilestoneRef references a milestone from an issue. Number carries the platform's milestone addressing identifier as a string: the milestone *number* on GitHub, the platform milestone *ID* on GitLab, Gitea, Forgejo, GitCode, and Tencent Code (whose write endpoints take exactly that identifier, so per-platform round-trips hold), and Gitee's milestone *serial number* (the "number" field of Gitee's milestone payload — the identifier Gitee's own issue and milestone write endpoints take; the SDK model exposes no id). This is the same identifier Milestone.Number exposes and MilestoneManager methods accept, so refs round-trip through the milestone manager on the platform they came from.

type MilestoneState

type MilestoneState string

MilestoneState represents the state of a milestone.

const (
	MilestoneStateOpen   MilestoneState = "open"
	MilestoneStateClosed MilestoneState = "closed"
)

type NormalizedEvent

type NormalizedEvent struct {
	ID        string         `json:"id"`
	Type      string         `json:"type"`
	Source    Platform       `json:"source"`
	Timestamp time.Time      `json:"timestamp"`
	Actor     *CRUser        `json:"actor"`
	Repo      *EventRepo     `json:"repo"`
	CR        *ChangeRequest `json:"cr,omitempty"`
	// Issue is set for issue.* events and for comments made on issues;
	// Comment carries the comment body for comment.* events. A comment on
	// a change request populates CR instead of (or alongside) nothing.
	Issue      *Issue          `json:"issue,omitempty"`
	Comment    *IssueComment   `json:"comment,omitempty"`
	Branch     string          `json:"branch,omitempty"`
	Tag        string          `json:"tag,omitempty"`
	CommitSHA  string          `json:"commit_sha,omitempty"`
	Action     string          `json:"action,omitempty"`
	RawPayload json.RawMessage `json:"raw_payload"`
}

NormalizedEvent represents a normalized webhook event from any platform.

type NoteManager added in v0.67.2

type NoteManager interface {
	// UpdateNote rewrites the body of one CR comment.
	UpdateNote(ctx context.Context, owner, repo, number, noteID, body string) (*IssueComment, error)
	// ListNotes returns every comment on the CR, oldest first (all pages).
	ListNotes(ctx context.Context, owner, repo, number string) ([]*IssueComment, error)
}

NoteManager provides targeted update and full pagination over CR (MR/PR) comments — an optional capability (v0.67.2). Semantics are anchored to the CR comment thread: on GitLab these hit the MergeRequest Notes API, because IssueManager's comment methods go through the Issues Notes API and always 404 for merge requests (MRs and issues live in separate iid namespaces). Platforms where an issue IS the PR (GitHub, Gitea, Gitee) are already covered by IssueManager and need not implement this interface.

type Notification

type Notification struct {
	ID        string              `json:"id"`
	Unread    bool                `json:"unread"`
	Reason    string              `json:"reason"`
	Subject   NotificationSubject `json:"subject"`
	Repo      *EventRepo          `json:"repo,omitempty"`
	UpdatedAt time.Time           `json:"updated_at"`
}

Notification represents a single notification from a user's inbox.

type NotificationManager

type NotificationManager interface {
	// ListNotifications returns the authenticated user's notifications,
	// exhausting the platform's pagination.
	ListNotifications(ctx context.Context, opts ListNotificationsOptions) ([]*Notification, error)
	// ListRepoNotifications returns notifications for a specific repository.
	ListRepoNotifications(ctx context.Context, owner, repo string, opts ListNotificationsOptions) ([]*Notification, error)
	// MarkNotificationRead marks a single notification thread as read.
	MarkNotificationRead(ctx context.Context, threadID string) error
	// MarkNotificationsRead marks all (or filtered) notifications as read.
	MarkNotificationsRead(ctx context.Context, opts MarkNotificationsOptions) error
	// MarkRepoNotificationsRead marks all notifications for a repository as read.
	MarkRepoNotificationsRead(ctx context.Context, owner, repo string, opts MarkNotificationsOptions) error
}

NotificationManager provides access to a user's notification inbox and per-repository notification streams. It is an optional capability interface: consumers should gate on Provider.Capabilities().Notifications (or type-assert) before use.

Platform support: GitHub, GitCode, Gitea, Forgejo, Gitee. GitLab exposes only notification *settings* (no inbox) and TencentCode has no notification API at all — both report Notifications=false in CapabilitySet.

type NotificationSubject

type NotificationSubject struct {
	Title string `json:"title"`
	Type  string `json:"type"` // "Issue", "PullRequest", "Commit", etc.
	URL   string `json:"url"`
}

NotificationSubject describes the resource that triggered the notification.

type PageFetch added in v0.72.0

type PageFetch[T any] func(ctx context.Context, page int) ([]T, error)

PageFetch fetches one page of a paginated list. page is the 1-based page number; the implementation must forward it into the platform call's page options (and should set an explicit PerPage so page sizes stay stable across the walk):

err := provider.Each(ctx, func(ctx context.Context, page int) ([]provider.PlatformRepo, error) {
    return p.ListRepos(ctx, provider.ListRepoOptions{Owner: "org", Page: page, PerPage: 100})
}, func(r provider.PlatformRepo) error {
    fmt.Println(r.FullName)
    return nil
})

type Platform

type Platform string

Platform represents a Git hosting platform.

const (
	PlatformGitLab      Platform = "gitlab"
	PlatformGitHub      Platform = "github"
	PlatformGitea       Platform = "gitea"
	PlatformGitee       Platform = "gitee"
	PlatformForgejo     Platform = "forgejo"
	PlatformTencentCode Platform = "tencent_code"
	PlatformGitCode     Platform = "gitcode"
)

func RegisteredPlatforms

func RegisteredPlatforms() []Platform

RegisteredPlatforms returns a list of all registered platforms.

type PlatformBranch

type PlatformBranch struct {
	Name string `json:"name"`
}

PlatformBranch represents a branch on a platform.

type PlatformRepo

type PlatformRepo struct {
	ID            int64    `json:"id"`
	FullName      string   `json:"full_name"`
	Name          string   `json:"name"`
	Owner         string   `json:"owner"`
	Description   string   `json:"description"`
	CloneURL      string   `json:"clone_url"`
	SSHURL        string   `json:"ssh_url"`
	DefaultBranch string   `json:"default_branch"`
	Private       bool     `json:"private"`
	Platform      Platform `json:"platform"`
	// 仓库元数据(用于导入过滤:排除 archived/fork、按 star/语言过滤)
	Archived bool   `json:"archived,omitempty"`
	Fork     bool   `json:"fork,omitempty"`
	Stars    int    `json:"stars,omitempty"`
	Language string `json:"language,omitempty"`
}

PlatformRepo represents a repository on a platform.

type PlatformWebhook

type PlatformWebhook struct {
	ID     int64    `json:"id"`
	URL    string   `json:"url"`
	Events []string `json:"events"`
}

PlatformWebhook represents a webhook on a platform.

type Provider

type Provider interface {
	// Platform returns the platform type.
	Platform() Platform
	// TestConnection verifies the connection and checks capabilities.
	TestConnection(ctx context.Context) (*TestConnectionResult, error)
	// Capabilities statically declares the optional capability interfaces
	// this provider implements. See CapabilitySet.
	Capabilities() CapabilitySet
	// Divergences statically declares this backend's divergence ledger: the
	// registered places where its behavior departs from the unified
	// semantics (stub / ignore / mapping / detour). Consumers can route on
	// these entries — e.g. provider.Ignores — and docs/divergence-ledger.md
	// is generated from them. Like CapabilitySet this is a compile-time
	// declaration; the contract suite locks ledger entries to behavior.
	Divergences() []Divergence

	RepoManager
	ChangeRequestManager
	WebhookManager
	BranchManager
	DiffManager
	CommitManager
	FileManager
	ReleaseManager
}

Provider is the unified interface for all Git hosting platforms. It composes 8 focused sub-interfaces for high cohesion and low coupling.

Consumers can depend on smaller interfaces (e.g., WebhookManager) when they don't need full Provider capabilities.

IssueManager, SearchManager, and CommitStatusManager are NOT part of Provider: only some platforms support them. Consumers that need issues, search, or commit statuses should type-assert against the optional capability interfaces:

if ism, ok := p.(provider.IssueManager); ok { ... }
if sm, ok := p.(provider.SearchManager); ok { ... }
if csm, ok := p.(provider.CommitStatusManager); ok { ... }

Caps declares the same information programmatically: consumers can route
on p.Capabilities() instead of probing with type assertions.

func NewProvider

func NewProvider(cfg Config) (Provider, error)

NewProvider creates a Provider for the given platform using the registry. Returns ErrPlatformNotSupported if the platform is not registered.

Platform backends are registered via init() functions. Import "github.com/yi-nology/go-git-platform/backends/all" with a blank identifier to register every platform shipped with the SDK.

type ProviderConstructor

type ProviderConstructor func(cfg Config) (Provider, error)

ProviderConstructor is a function that creates a Provider from a Config.

type ProviderError

type ProviderError struct {
	Platform   Platform
	Op         string // operation name, e.g., "ListRepos"
	Resource   string // optional resource identifier, e.g., "owner/repo"
	StatusCode int    // 0 when not applicable (e.g. configuration errors)
	// RetryAfter / ResetAt carry the server-advised recovery window when
	// the error is a rate-limit rejection and the platform sent hints
	// (Retry-After header; X-RateLimit-Reset unix seconds). Zero when
	// absent. Copied from the underlying transport error by Wrap; see
	// RateLimitRecovery for the ergonomic accessor.
	RetryAfter time.Duration
	ResetAt    time.Time
	Cause      error // underlying cause; nil when constructed from raw fields
}

ProviderError is a structured error from a provider operation. It carries enough context (platform, op, resource, status code, cause) to support logging, retry, and user-facing messages. It implements errors.Is so the sentinel errors above are matched by Cause, and errors.As for typed access.

func (*ProviderError) Error

func (e *ProviderError) Error() string

Error implements the error interface.

func (*ProviderError) Is

func (e *ProviderError) Is(target error) bool

Is implements errors.Is. It matches when the wrapped Cause equals the target, which lets callers write `errors.Is(err, provider.ErrNotFound)`.

func (*ProviderError) IsClientError

func (e *ProviderError) IsClientError() bool

IsClientError reports 4xx.

func (*ProviderError) IsServerError

func (e *ProviderError) IsServerError() bool

IsServerError reports 5xx.

func (*ProviderError) IsStatus

func (e *ProviderError) IsStatus(code int) bool

IsStatus reports whether the error has the given HTTP status code.

func (*ProviderError) Unwrap

func (e *ProviderError) Unwrap() error

Unwrap implements errors.Unwrap.

type Reaction

type Reaction struct {
	ID    int64   `json:"id"`
	Emoji string  `json:"emoji"`
	User  *CRUser `json:"user,omitempty"`
}

Reaction represents an emoji reaction on an issue or comment.

type ReactionManager

type ReactionManager interface {
	// ListIssueReactions returns all reactions on an issue.
	ListIssueReactions(ctx context.Context, owner, repo, number string) ([]*Reaction, error)
	// AddIssueReaction adds a reaction emoji to an issue. emoji is one of
	// the Reaction* constants (e.g. ReactionHeart).
	AddIssueReaction(ctx context.Context, owner, repo, number, emoji string) (*Reaction, error)
	// RemoveIssueReaction removes a reaction by its platform ID.
	RemoveIssueReaction(ctx context.Context, owner, repo, number string, reactionID int64) error

	// ListIssueCommentReactions returns all reactions on an issue comment.
	ListIssueCommentReactions(ctx context.Context, owner, repo string, commentID int64) ([]*Reaction, error)
	// AddIssueCommentReaction adds a reaction emoji to an issue comment.
	AddIssueCommentReaction(ctx context.Context, owner, repo string, commentID int64, emoji string) (*Reaction, error)
	// RemoveIssueCommentReaction removes a reaction by its platform ID.
	RemoveIssueCommentReaction(ctx context.Context, owner, repo string, commentID int64, reactionID int64) error

	// ListCRReactions returns all reactions on a change request (PR/MR).
	ListCRReactions(ctx context.Context, owner, repo, number string) ([]*Reaction, error)
	// ListCRCommentReactions returns all reactions on a change-request
	// comment (PR review comment / MR note).
	ListCRCommentReactions(ctx context.Context, owner, repo string, commentID int64) ([]*Reaction, error)
	// AddCRCommentReaction adds a reaction emoji to a change-request comment.
	AddCRCommentReaction(ctx context.Context, owner, repo string, commentID int64, emoji string) (*Reaction, error)
	// RemoveCRCommentReaction removes a reaction by its platform ID.
	RemoveCRCommentReaction(ctx context.Context, owner, repo string, commentID int64, reactionID int64) error
}

ReactionManager provides emoji reaction CRUD on issues and their comments (both issue comments and change-request comments). It is an optional capability interface: consumers should gate on Provider.Capabilities().Reactions (or type-assert) before use.

Platform support: GitHub, GitCode, GitLab (via award-emoji), Gitea, Forgejo. Gitee and TencentCode have no reaction API — both report Reactions=false in CapabilitySet.

GitLab maps reactions to its "award emoji" API; the SDK normalizes emoji names (e.g. +1 ↔ thumbsup, hooray ↔ tada) so callers use the same constants across all platforms.

type ReleaseAsset added in v0.72.0

type ReleaseAsset struct {
	ID                 int64  `json:"id"`
	Name               string `json:"name"`
	Size               int64  `json:"size"`
	ContentType        string `json:"content_type"`
	BrowserDownloadURL string `json:"browser_download_url"`
	URL                string `json:"url"`
}

ReleaseAsset Release 附件元数据。json tag 与 GitHub API 对齐,与消费方 (git-sync-service githubapi.ReleaseAsset)字段保持兼容,Size 放大到 int64。

type ReleaseAssetManager added in v0.72.0

type ReleaseAssetManager interface {
	// DownloadReleaseAsset 下载 assetID 对应附件并流式写入 w。
	// 非 2xx 响应或无响应体时返回错误。
	DownloadReleaseAsset(ctx context.Context, owner, repo string, assetID int64, w io.Writer) error
}

ReleaseAssetManager 下载 Release 附件。可选能力接口:使用前先看 Provider.Capabilities().ReleaseAssets(或类型断言)。附件可能很大, 因此是流式接口而非返回 []byte,避免整读内存。

type ReleaseInfo

type ReleaseInfo struct {
	ID          int64     `json:"id"`
	TagName     string    `json:"tag_name"`
	Title       string    `json:"title"`
	Body        string    `json:"body"`
	URL         string    `json:"url"`
	Draft       bool      `json:"draft"`
	Prerelease  bool      `json:"prerelease"`
	CreatedAt   time.Time `json:"created_at"`
	PublishedAt time.Time `json:"published_at"`
	// Assets 发布附件元数据(仅声明 ReleaseAssets 能力的后端填充)。
	Assets []*ReleaseAsset `json:"assets,omitempty"`
}

ReleaseInfo represents a release.

type ReleaseManager

type ReleaseManager interface {
	ListTags(ctx context.Context, owner, repo string) ([]*TagInfo, error)
	ListReleases(ctx context.Context, owner, repo string) ([]*ReleaseInfo, error)
	CreateRelease(ctx context.Context, owner, repo string, opts CreateReleaseOptions) (*ReleaseInfo, error)
	GetReleaseByTag(ctx context.Context, owner, repo, tag string) (*ReleaseInfo, error)
	UpdateRelease(ctx context.Context, owner, repo, tag string, opts UpdateReleaseOptions) (*ReleaseInfo, error)
	DeleteRelease(ctx context.Context, owner, repo, tag string) error
	GetArchive(ctx context.Context, owner, repo, ref, format string) ([]byte, error)
}

ReleaseManager handles tags, releases, and archives. Releases are addressed by tag name across every method: tag names are stable and human-addressable, while the underlying numeric release IDs are not.

type RepoManager

type RepoManager interface {
	ListRepos(ctx context.Context, opts ListRepoOptions) ([]*PlatformRepo, error)
	GetRepo(ctx context.Context, owner, repo string) (*PlatformRepo, error)
	CreateRepo(ctx context.Context, owner string, opts CreateRepoOptions) (*PlatformRepo, error)
	DeleteRepo(ctx context.Context, owner, repo string) error
	UpdateRepo(ctx context.Context, owner, repo string, opts UpdateRepoOptions) (*PlatformRepo, error)
	ForkRepo(ctx context.Context, owner, repo string, opts ForkRepoOptions) (*PlatformRepo, error)
}

RepoManager handles repository CRUD operations.

type RepoStatsManager

type RepoStatsManager interface {
	// ListForks returns all forks of the repository.
	ListForks(ctx context.Context, owner, repo string) ([]*PlatformRepo, error)
	// ListStargazers returns users who have starred the repository.
	ListStargazers(ctx context.Context, owner, repo string) ([]*CRUser, error)
	// ListContributors returns contributors ranked by commit count.
	ListContributors(ctx context.Context, owner, repo string) ([]*Contributor, error)
}

RepoStatsManager provides read-only repository statistics: forks, stargazers, and contributors. It is an optional capability interface: consumers should gate on Provider.Capabilities().RepoStats (or type-assert) before use.

Not every backend implements every method; backends that lack a specific method return ErrNotImplemented and register the gap in their divergence ledger. A backend declares RepoStats=true if it implements at least one method.

type RequestHook

type RequestHook func(ctx context.Context, req *http.Request) context.Context

RequestHook is called before an HTTP request is sent. It can modify the context (e.g., add tracing headers) or inspect the request.

type ResponseHook

type ResponseHook func(ctx context.Context, req *http.Request, resp *http.Response, duration time.Duration, err error)

ResponseHook is called after an HTTP response is received.

type RetryConfig

type RetryConfig struct {
	// MaxRetries is the maximum number of retry attempts (not counting the
	// initial request). <= 0 disables retry.
	MaxRetries int
	// BaseDelay is the initial backoff delay before the first retry.
	BaseDelay time.Duration
	// MaxDelay caps the backoff delay. Zero means 30s (the transport
	// default).
	MaxDelay time.Duration
	// RetryOn lists extra HTTP status codes to retry on, in addition to the
	// transport's default set (429 and 5xx).
	RetryOn []int
}

RetryConfig controls automatic retry behavior for HTTP requests issued by the transport layer. It is the public-facing configuration type passed via provider.Config; the transport package has its own internal transport.RetryConfig that this is mapped into.

func DefaultRetryConfig

func DefaultRetryConfig() RetryConfig

DefaultRetryConfig returns a sensible default retry configuration: 3 retries, 500ms base delay, 30s cap, no extra status codes.

type Review

type Review struct {
	ID          int64       `json:"id"`
	User        string      `json:"user"`
	State       ReviewState `json:"state"`
	Body        string      `json:"body"`
	SubmittedAt time.Time   `json:"submitted_at"`
}

Review represents a code review on a change request (the ReviewManager view; ReviewResult above is the create-call response).

type ReviewComment

type ReviewComment struct {
	Path      string `json:"path"`
	Body      string `json:"body"`
	Line      int    `json:"line,omitempty"`
	StartLine int    `json:"start_line,omitempty"`
	EndLine   int    `json:"end_line,omitempty"`
	Side      string `json:"side,omitempty"`
}

ReviewComment is a single inline comment in a code review.

func (*ReviewComment) Mentions

func (c *ReviewComment) Mentions() []string

Mentions returns the deduplicated @usernames found in the comment body.

type ReviewCommentResult

type ReviewCommentResult struct {
	Path       string `json:"path,omitempty"`
	Line       int    `json:"line,omitempty"`
	ExternalID string `json:"external_id,omitempty"`
	Error      string `json:"error,omitempty"`
}

ReviewCommentResult is the result of posting a single inline comment.

type ReviewManager

type ReviewManager interface {
	CreateReview(ctx context.Context, owner, repo, number string, opts CreateReviewOptions) (*ReviewResult, error)
	ListReviews(ctx context.Context, owner, repo, number string) ([]Review, error)
	GetReview(ctx context.Context, owner, repo, number string, reviewID int64) (*Review, error)
	RequestReviewers(ctx context.Context, owner, repo, number string, reviewers []string) error
	DismissReview(ctx context.Context, owner, repo, number string, reviewID int64, message string) error
}

ReviewManager provides code-review operations on change requests. Change request numbers are strings (same addressing scheme as IssueManager); individual reviews are addressed by their numeric platform ID. It is an optional capability: consumers should check Capabilities().Reviews before type-asserting.

type ReviewResult

type ReviewResult struct {
	ID       string                `json:"id"`
	Body     string                `json:"body,omitempty"`
	HTMLURL  string                `json:"html_url,omitempty"`
	User     *CRUser               `json:"user,omitempty"`
	Comments []ReviewCommentResult `json:"comments,omitempty"`
}

ReviewResult is the result of a CreateReview call.

type ReviewState

type ReviewState string

ReviewState is the normalized state of a code review.

const (
	ReviewStateApproved         ReviewState = "approved"
	ReviewStateChangesRequested ReviewState = "changes_requested"
	ReviewStateCommented        ReviewState = "commented"
	ReviewStatePending          ReviewState = "pending"
)

type SearchIssueResult

type SearchIssueResult struct {
	Number    string     `json:"number"`
	Title     string     `json:"title"`
	Body      string     `json:"body,omitempty"`
	State     IssueState `json:"state"`
	WebURL    string     `json:"web_url,omitempty"`
	Labels    []string   `json:"labels,omitempty"`
	Comments  int        `json:"comments,omitempty"`
	CreatedAt time.Time  `json:"created_at"`
}

SearchIssueResult is a single result from an issue search. Number is the platform's issue addressing identifier as a string, so results feed GetIssue(number string) directly (numeric platforms return "1", Gitee's alphanumeric identifiers return e.g. "IAINVA").

type SearchIssuesOptions

type SearchIssuesOptions struct {
	Query   string `json:"q"`
	Repo    string `json:"repo,omitempty"`
	State   string `json:"state,omitempty"`
	Sort    string `json:"sort,omitempty"`
	Order   string `json:"order,omitempty"`
	Page    int    `json:"page,omitempty"`
	PerPage int    `json:"per_page,omitempty"`
}

SearchIssuesOptions contains options for searching issues.

type SearchManager

type SearchManager interface {
	SearchRepos(ctx context.Context, opts SearchReposOptions) ([]*SearchRepoResult, *int, error)
	SearchIssues(ctx context.Context, opts SearchIssuesOptions) ([]*SearchIssueResult, *int, error)
	SearchUsers(ctx context.Context, opts SearchUsersOptions) ([]*SearchUserResult, *int, error)
}

SearchManager provides cross-platform search for repositories, issues, and users.

The *int return is the server-side total when the platform reports one, and nil when it does not — callers must not treat a total as guaranteed.

type SearchRepoResult

type SearchRepoResult struct {
	FullName      string `json:"full_name"`
	Description   string `json:"description,omitempty"`
	WebURL        string `json:"web_url,omitempty"`
	Stars         int    `json:"stars,omitempty"`
	Forks         int    `json:"forks,omitempty"`
	DefaultBranch string `json:"default_branch,omitempty"`
	Private       bool   `json:"private,omitempty"`
}

SearchRepoResult is a single result from a repository search.

type SearchReposOptions

type SearchReposOptions struct {
	Query   string `json:"q"`
	Sort    string `json:"sort,omitempty"`
	Order   string `json:"order,omitempty"`
	Page    int    `json:"page,omitempty"`
	PerPage int    `json:"per_page,omitempty"`
}

SearchReposOptions contains options for searching repositories.

Sort and Order are platform-dependent: each backend forwards them to its platform's own vocabulary (e.g. GitHub's stars/forks/updated with asc/desc), so values valid on one platform may be ignored or rejected on another (gitea/forgejo reject unknown sort/order values with HTTP 422; gitlab's search API exposes no sort/order at all — a registered ignore). Consult the target platform's search documentation for the accepted values.

type SearchUserResult

type SearchUserResult struct {
	Login     string `json:"login"`
	Name      string `json:"name,omitempty"`
	AvatarURL string `json:"avatar_url,omitempty"`
	WebURL    string `json:"web_url,omitempty"`
}

SearchUserResult is a single result from a user search.

type SearchUsersOptions

type SearchUsersOptions struct {
	Query   string `json:"q"`
	Sort    string `json:"sort,omitempty"`
	Order   string `json:"order,omitempty"`
	Page    int    `json:"page,omitempty"`
	PerPage int    `json:"per_page,omitempty"`
}

SearchUsersOptions contains options for searching users.

type StarredManager added in v0.72.0

type StarredManager interface {
	// ListStarred 分页列出认证用户 star 的仓库(GET /user/starred)。
	// page/perPage 语义与其他分页方法一致,非法值经 NormalizePageOpts 归一。
	ListStarred(ctx context.Context, page, perPage int) ([]*PlatformRepo, error)
}

StarredManager 列出认证用户 star 过的仓库。可选能力接口:使用前先看 Provider.Capabilities().Starred(或类型断言)。

直接复用 PlatformRepo 而非再造摘要类型:下游导入/org 镜像场景只消费 FullName/Name/Owner/CloneURL/Description/Archived/Fork/Stars/Language/ Private 这些现成字段,再造一套会引入无谓的字段映射漂移。GitHub 专属 (无跨平台抽象价值),故仅 github 后端实现。

type StaticTokenValidator

type StaticTokenValidator struct {
	Header string
}

StaticTokenValidator compares a static token header against the configured secret in constant time. Used for GitLab's X-Gitlab-Token.

func (StaticTokenValidator) Name

Name implements WebhookValidator.

func (StaticTokenValidator) Validate

func (s StaticTokenValidator) Validate(r *http.Request, body []byte, secret string) error

Validate implements WebhookValidator.

type Stats

type Stats struct {
	Hits      int64
	Misses    int64
	Evictions int64
	Size      int
}

Stats reports cache hit/miss counters and the current size. Counters are atomically incremented and safe to read concurrently.

type StatusError

type StatusError struct {
	Status int
	Cause  error
}

StatusError wraps an error with an explicit HTTP status code. Use this in platform backends when you need to attach a status code to an error from a third-party SDK that doesn't implement the statusCoder interface. This avoids the need for the reflection-based fallback in Wrap.

Example:

err := someSDK.DoSomething()
if err != nil {
    return provider.WrapStatusError(err, 404)
}

func (*StatusError) Error

func (e *StatusError) Error() string

func (*StatusError) StatusCode

func (e *StatusError) StatusCode() int

func (*StatusError) Unwrap

func (e *StatusError) Unwrap() error

type StatusExtractor added in v0.83.0

type StatusExtractor func(err error) (int, bool)

StatusExtractor reports the HTTP status carried by one of a platform's SDK error types. It is called per link of the chain Wrap walks, so the extractor only needs to handle its own error values (the chain walk handles wrapping).

type TagInfo

type TagInfo struct {
	Name   string `json:"name"`
	Commit string `json:"commit"`
}

TagInfo represents a tag.

type TestConnectionResult

type TestConnectionResult struct {
	Connected    bool   `json:"connected"`
	Platform     string `json:"platform"`
	UserName     string `json:"user_name"`
	Message      string `json:"message,omitempty"`
	CanListRepos bool   `json:"can_list_repos"`
	CanReadCR    bool   `json:"can_read_cr"`
	CanWriteCR   bool   `json:"can_write_cr"`
	CanWebhook   bool   `json:"can_webhook"`
}

TestConnectionResult contains the result of a connection test.

type TokenSource added in v0.72.0

type TokenSource interface {
	Token(ctx context.Context) (string, error)
}

TokenSource supplies the access token used to authenticate platform API requests. When a Config.TokenSource is set, the transport layer consults it on every request, so rotating or expiring credentials — OAuth device-flow tokens, GitHub App installation tokens (hourly expiry), short-lived CI job tokens — take effect on the next request without rebuilding the provider.

The interface is deliberately identical to transport.TokenSource, so any implementation satisfies both and backends can forward the value unchanged. Implementations must be safe for concurrent use.

func StaticTokenSource added in v0.72.0

func StaticTokenSource(token string) TokenSource

StaticTokenSource returns a TokenSource that always returns token; it is the TokenSource equivalent of Config.Token and mostly useful for tests and for swapping a static credential into TokenSource-taking code paths.

type UpdateBranchProtectionOptions

type UpdateBranchProtectionOptions struct {
	RequiredApprovingReviews *int  `json:"required_approving_reviews,omitempty"`
	RequiredStatusChecks     *bool `json:"required_status_checks,omitempty"`
	AllowForcePushes         *bool `json:"allow_force_pushes,omitempty"`
	AllowDeletions           *bool `json:"allow_deletions,omitempty"`
}

UpdateBranchProtectionOptions contains options for updating a branch protection rule. Nil fields are left unchanged.

type UpdateCROptions

type UpdateCROptions struct {
	Title        string `json:"title,omitempty"`
	Description  string `json:"description,omitempty"`
	TargetBranch string `json:"target_branch,omitempty"`
}

UpdateCROptions contains options for updating a change request.

type UpdateIssueOptions

type UpdateIssueOptions struct {
	Title     string     `json:"title,omitempty"`
	Body      string     `json:"body,omitempty"`
	State     IssueState `json:"state,omitempty"`
	Assignees []string   `json:"assignees,omitempty"`
	Labels    []string   `json:"labels,omitempty"`
	Milestone string     `json:"milestone,omitempty"` // milestone number/ID as a string; "" = leave unchanged
}

UpdateIssueOptions contains options for updating an issue.

type UpdateLabelOptions

type UpdateLabelOptions struct {
	NewName     *string `json:"new_name,omitempty"`
	Color       *string `json:"color,omitempty"`
	Description *string `json:"description,omitempty"`
}

UpdateLabelOptions contains options for updating a repository label. Nil fields are left unchanged.

type UpdateMilestoneOptions

type UpdateMilestoneOptions struct {
	Title       *string        `json:"title,omitempty"`
	Description *string        `json:"description,omitempty"`
	State       MilestoneState `json:"state,omitempty"`
	DueOn       *time.Time     `json:"due_on,omitempty"`
}

UpdateMilestoneOptions contains options for updating a repository milestone. Nil fields are left unchanged.

type UpdateReleaseOptions

type UpdateReleaseOptions struct {
	Name       *string `json:"name,omitempty"`
	Body       *string `json:"body,omitempty"`
	Draft      *bool   `json:"draft,omitempty"`
	Prerelease *bool   `json:"prerelease,omitempty"`
}

UpdateReleaseOptions contains options for updating a release addressed by tag. Nil fields are left unchanged.

type UpdateRepoOptions

type UpdateRepoOptions struct {
	Name          string `json:"name,omitempty"`
	Description   string `json:"description,omitempty"`
	DefaultBranch string `json:"default_branch,omitempty"`
	Private       *bool  `json:"private,omitempty"`
}

UpdateRepoOptions contains options for updating a repository.

type UserManager

type UserManager interface {
	// GetUser returns the user profile for the given username.
	GetUser(ctx context.Context, username string) (*CRUser, error)

	// ResolveUsernames maps a list of usernames to their platform IDs.
	// The returned slice preserves input order; each entry's ID field
	// carries the resolved numeric ID. If a username cannot be resolved,
	// an error is returned for that entry (the partial result up to that
	// point is still valid).
	ResolveUsernames(ctx context.Context, usernames []string) ([]*CRUser, error)
}

UserManager provides user lookup and username-to-ID resolution. It is an optional capability interface: consumers should gate on Provider.Capabilities().Users (or type-assert) before use.

Not every backend needs this — GitHub, Gitea, Forgejo, Gitee, and GitCode accept usernames directly in their APIs. GitLab and TencentCode require numeric user IDs for assignees and reviewers, so they implement this interface to expose their resolution logic.

Consumers can also use this interface to look up user profiles or validate that a username exists before passing it to other operations.

type ValidatorFunc

type ValidatorFunc struct {
	N  string
	Fn func(r *http.Request, body []byte, secret string) error
}

ValidatorFunc adapts a plain function into a WebhookValidator.

func (ValidatorFunc) Name

func (v ValidatorFunc) Name() string

Name implements WebhookValidator.

func (ValidatorFunc) Validate

func (v ValidatorFunc) Validate(r *http.Request, body []byte, secret string) error

Validate implements WebhookValidator.

type WaitOptions

type WaitOptions struct {
	// Interval is the poll cadence; defaults to DefaultWaitInterval and
	// is clamped up to minWaitInterval.
	Interval time.Duration
	// Timeout bounds the whole wait. Zero uses DefaultWaitTimeout; a
	// negative value disables the budget entirely (rely on your own
	// context deadline). Expiry surfaces as ErrWaitTimedOut carrying the
	// last observed combined state.
	Timeout time.Duration
	// Contexts, when non-empty, restricts the combined state to the
	// named status contexts (e.g. []string{"ci/lint", "ci/test"}).
	// Empty means every context reported on the commit.
	Contexts []string
	// InitialDelay defers the first poll; useful right after
	// CreateCommitStatus, whose result may not be queryable for a
	// moment. Defaults to no delay.
	InitialDelay time.Duration
}

WaitOptions tunes the polling loop in WaitForCommitStatus.

type WebhookEnvelope added in v0.83.0

type WebhookEnvelope struct {
	// Platform selects the registered signature validator.
	Platform Platform
	// EventHeaders are checked in order for the event-type header; the first
	// non-empty value wins (multi-header dialects list fallbacks). Leave
	// empty when the platform dispatches on a payload field (e.g. GitLab's
	// object_kind) instead of a header.
	EventHeaders []string
	// IDPrefix prefixes generated event IDs ("<prefix>-<unixnano>-<key>").
	// Empty falls back to the platform name — the ID is **never empty**
	// (see NewEvent).
	IDPrefix string
	// IDHeaders are checked in order for the platform's **delivery ID**
	// (e.g. GitHub's "X-GitHub-Delivery"); the first non-empty value wins.
	//
	// The delivery ID is a true idempotency key: the platform repeats the
	// same value when it redelivers an event, so callers can deduplicate
	// properly. Without it the ID is synthesized locally and differs on
	// every delivery (no deduplication possible).
	IDHeaders []string
}

WebhookEnvelope carries one platform's webhook dialect for the shared envelope steps every ParseWebhookEvent must run before its payload switch (validate → read/restore body → event header → event skeleton). Payload decoding and the event-type switch stay per-platform: those are the dialect, not the envelope.

func (WebhookEnvelope) NewEvent added in v0.83.0

func (e WebhookEnvelope) NewEvent(idKey int64, body []byte) *NormalizedEvent

NewEvent builds the event skeleton: Source, Timestamp, RawPayload — plus ID "<prefix>-<unixnano>-<idKey>". idKey is the platform's payload identifier for the event (issue number, MR IID, ...). Actor/Repo remain the caller's: they come from the payload the mapper decodes.

The ID is **never empty**: without an explicit IDPrefix the platform name is used as the prefix. An empty event ID is not merely "missing data" — callers commonly key their dedup table and their unique index on it, and an empty key makes every later event collide with the first one (silently dropping all events after the first).

func (WebhookEnvelope) NewEventWithID added in v0.99.3

func (e WebhookEnvelope) NewEventWithID(r *http.Request, idKey int64, body []byte) *NormalizedEvent

NewEventWithID behaves like NewEvent but prefers the platform's delivery ID (X-GitHub-Delivery and friends) as the event ID.

The delivery ID is a real idempotency key: the platform repeats it when it redelivers the same event, so a caller keying dedup on the event ID can actually suppress the duplicate. Falls back to NewEvent's synthesized ID when the platform sends no such header.

func (WebhookEnvelope) Read added in v0.83.0

func (e WebhookEnvelope) Read(r *http.Request, secret string) (string, []byte, error)

Read validates the signature via the platform's registered validator, restores the body for downstream parsing, and returns the event-type header ("" when EventHeaders is empty or unmatched) plus the raw payload. Error ops mirror the historical hand-written sequence: body-read failures surface under ParseWebhookEvent, validation failures under ValidateWebhookSignature.

Passing secret == SkipValidationSecret skips the signature validation step: see that constant for the hosts that need parse-only access.

type WebhookManager

type WebhookManager interface {
	CreateWebhook(ctx context.Context, opts CreateWebhookOptions) (*PlatformWebhook, error)
	DeleteWebhook(ctx context.Context, owner, repo string, webhookID int64) error
	ListWebhooks(ctx context.Context, owner, repo string) ([]*PlatformWebhook, error)
	ParseWebhookEvent(r *http.Request, secret string) (*NormalizedEvent, error)
	ValidateWebhookSignature(r *http.Request, secret string) error
}

WebhookManager handles webhook CRUD and event parsing.

type WebhookValidator

type WebhookValidator interface {
	Name() string
	Validate(r *http.Request, body []byte, secret string) error
}

WebhookValidator verifies the authenticity of an incoming webhook request. Implementations are stateless and safe for concurrent use.

The signature scheme varies per platform:

  • GitHub: HMAC-SHA256 of the body, sent in X-Hub-Signature-256.
  • GitLab: static token compared in constant time against X-Gitlab-Token.
  • Gitea / Forgejo: HMAC-SHA256 of the body, sent in X-Gitea-Signature.
  • GitCode: static password compared in constant time against X-GitCode-Token (an HMAC sign mode exists on the platform; its message format is not yet mirrored here).
  • Tencent: static token compared in constant time against X-Token.
  • Gitee: sign mode — Base64(HMAC-SHA256(secret, timestamp+"\n"+ secret)) in X-Gitee-Token with X-Gitee-Timestamp; or password mode — the plain password in X-Gitee-Token.

type WebhookValidatorRegistry

type WebhookValidatorRegistry struct {
	// contains filtered or unexported fields
}

WebhookValidatorRegistry indexes WebhookValidator implementations by platform. It is safe for concurrent use; registrations happen during package init and reads happen on the request path.

func DefaultWebhookRegistry

func DefaultWebhookRegistry() *WebhookValidatorRegistry

DefaultWebhookRegistry returns the process-wide registry.

func NewWebhookValidatorRegistry

func NewWebhookValidatorRegistry() *WebhookValidatorRegistry

NewWebhookValidatorRegistry builds an empty registry.

func (*WebhookValidatorRegistry) Get

Get returns the validator for the given platform, or nil if none is registered.

func (*WebhookValidatorRegistry) Register

Register associates a validator with a platform.

func (*WebhookValidatorRegistry) Validate

func (r *WebhookValidatorRegistry) Validate(p Platform, req *http.Request, body []byte, secret string) error

Validate looks up the validator for the given platform and runs it. A nil validator or missing platform produces ErrNotImplemented.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL