boilerplates

package module
v0.0.4-alpha Latest Latest
Warning

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

Go to latest
Published: Dec 30, 2025 License: AGPL-3.0 Imports: 26 Imported by: 12

README

utility-boilerplates

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CreateSBOMPlugin

func CreateSBOMPlugin(analyzer SBOMAnalyzer) error

CreateSBOMPlugin creates and starts a complete SBOM plugin with the given analyzer

func StartMetricsServer

func StartMetricsServer(port string)

StartMetricsServer starts the Prometheus metrics HTTP server

Types

type AMQPConfig

type AMQPConfig struct {
	URL      string `json:"url"`
	Protocol string `json:"protocol"`
	Host     string `json:"host"`
	Port     string `json:"port"`
	User     string `json:"user"`
	Password string `json:"password"`
}

AMQPConfig holds AMQP/RabbitMQ configuration

type AnalysisHandler

type AnalysisHandler interface {
	StartAnalysis(
		databases *PluginDatabases,
		dispatcherMessage types_amqp.DispatcherPluginMessage,
		config plugin_db.Plugin,
		analysisDoc codeclarity.Analysis,
	) (map[string]any, codeclarity.AnalysisStatus, error)
}

AnalysisHandler defines the interface that plugins must implement

type ConfigError

type ConfigError struct {
	Field   string `json:"field"`
	Message string `json:"message"`
}

ConfigError represents configuration-related errors

func (ConfigError) Error

func (e ConfigError) Error() string

type ConfigService

type ConfigService struct {
	Database DatabaseConfig `json:"database"`
	AMQP     AMQPConfig     `json:"amqp"`
	General  GeneralConfig  `json:"general"`
}

ConfigService centralizes all environment variable management and configuration

func CreateConfigService

func CreateConfigService() (*ConfigService, error)

CreateConfigService creates a new ConfigService by reading all environment variables

func (*ConfigService) GetDatabaseDSN

func (cs *ConfigService) GetDatabaseDSN(dbName string) string

GetDatabaseDSN constructs a PostgreSQL DSN for the specified database

func (*ConfigService) GetDatabaseTimeout

func (cs *ConfigService) GetDatabaseTimeout() time.Duration

GetDatabaseTimeout returns the configured database timeout

func (*ConfigService) GetEnvironmentInfo

func (cs *ConfigService) GetEnvironmentInfo() map[string]interface{}

GetEnvironmentInfo returns a summary of the current environment configuration

func (*ConfigService) GetLogLevel

func (cs *ConfigService) GetLogLevel() string

GetLogLevel returns the configured log level

func (*ConfigService) IsDevelopment

func (cs *ConfigService) IsDevelopment() bool

IsDevelopment returns true if running in development environment

func (*ConfigService) IsProduction

func (cs *ConfigService) IsProduction() bool

IsProduction returns true if running in production environment

func (*ConfigService) Validate

func (cs *ConfigService) Validate() error

Validate validates the configuration and returns any errors

type DatabaseConfig

type DatabaseConfig struct {
	Host     string        `json:"host"`
	Port     string        `json:"port"`
	User     string        `json:"user"`
	Password string        `json:"password"`
	Timeout  time.Duration `json:"timeout"`
}

DatabaseConfig holds database connection configuration

type EcosystemError

type EcosystemError struct {
	// Core error information
	Message   string    `json:"message"`
	Cause     error     `json:"cause,omitempty"`
	Timestamp time.Time `json:"timestamp"`

	// Context information
	Ecosystem  string `json:"ecosystem,omitempty"`
	Plugin     string `json:"plugin,omitempty"`
	Stage      string `json:"stage,omitempty"`
	AnalysisID string `json:"analysisId,omitempty"`

	// Error classification
	Severity    ErrorSeverity `json:"severity"`
	Category    ErrorCategory `json:"category"`
	Recoverable bool          `json:"recoverable"`

	// Additional context
	StackTrace string                 `json:"stackTrace,omitempty"`
	Metadata   map[string]interface{} `json:"metadata,omitempty"`
}

EcosystemError represents ecosystem-specific errors with rich context

func NewConfigurationError

func NewConfigurationError(message string, cause error) *EcosystemError

NewConfigurationError creates a configuration-related error

func NewDatabaseError

func NewDatabaseError(message string, cause error, ecosystem, plugin string) *EcosystemError

NewDatabaseError creates a database-related error

func NewEcosystemError

func NewEcosystemError(message string, cause error) *EcosystemError

NewEcosystemError creates a new ecosystem error

func NewNetworkError

func NewNetworkError(message string, cause error, ecosystem, plugin string) *EcosystemError

NewNetworkError creates a network-related error

func NewProcessingError

func NewProcessingError(message string, cause error, ecosystem, plugin, stage string) *EcosystemError

NewProcessingError creates a processing-related error

func NewValidationError

func NewValidationError(message string, ecosystem, plugin string) *EcosystemError

NewValidationError creates a validation-related error

func (*EcosystemError) Error

func (e *EcosystemError) Error() string

Error implements the error interface

func (*EcosystemError) IsCritical

func (e *EcosystemError) IsCritical() bool

IsCritical returns true if the error is critical

func (*EcosystemError) IsRecoverable

func (e *EcosystemError) IsRecoverable() bool

IsRecoverable returns true if the error is recoverable

func (*EcosystemError) Unwrap

func (e *EcosystemError) Unwrap() error

Unwrap allows error unwrapping

type EcosystemErrorBuilder

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

EcosystemErrorBuilder provides a fluent interface for building ecosystem errors

func NewErrorBuilder

func NewErrorBuilder(message string) *EcosystemErrorBuilder

NewErrorBuilder creates a new error builder

func (*EcosystemErrorBuilder) Build

Build creates the final ecosystem error

func (*EcosystemErrorBuilder) WithAnalysisID

func (eb *EcosystemErrorBuilder) WithAnalysisID(analysisID string) *EcosystemErrorBuilder

WithAnalysisID sets the analysis ID context

func (*EcosystemErrorBuilder) WithCategory

func (eb *EcosystemErrorBuilder) WithCategory(category ErrorCategory) *EcosystemErrorBuilder

WithCategory sets the error category

func (*EcosystemErrorBuilder) WithCause

func (eb *EcosystemErrorBuilder) WithCause(cause error) *EcosystemErrorBuilder

WithCause sets the underlying cause error

func (*EcosystemErrorBuilder) WithEcosystem

func (eb *EcosystemErrorBuilder) WithEcosystem(ecosystem string) *EcosystemErrorBuilder

WithEcosystem sets the ecosystem context

func (*EcosystemErrorBuilder) WithMetadata

func (eb *EcosystemErrorBuilder) WithMetadata(key string, value interface{}) *EcosystemErrorBuilder

WithMetadata adds metadata to the error

func (*EcosystemErrorBuilder) WithPlugin

func (eb *EcosystemErrorBuilder) WithPlugin(plugin string) *EcosystemErrorBuilder

WithPlugin sets the plugin context

func (*EcosystemErrorBuilder) WithRecoverable

func (eb *EcosystemErrorBuilder) WithRecoverable(recoverable bool) *EcosystemErrorBuilder

WithRecoverable sets whether the error is recoverable

func (*EcosystemErrorBuilder) WithSeverity

func (eb *EcosystemErrorBuilder) WithSeverity(severity ErrorSeverity) *EcosystemErrorBuilder

WithSeverity sets the error severity

func (*EcosystemErrorBuilder) WithStackTrace

func (eb *EcosystemErrorBuilder) WithStackTrace() *EcosystemErrorBuilder

WithStackTrace captures the current stack trace

func (*EcosystemErrorBuilder) WithStage

func (eb *EcosystemErrorBuilder) WithStage(stage string) *EcosystemErrorBuilder

WithStage sets the processing stage context

type ErrorCategory

type ErrorCategory string

ErrorCategory defines the category of error

const (
	ErrorCategoryDatabase      ErrorCategory = "database"
	ErrorCategoryNetwork       ErrorCategory = "network"
	ErrorCategoryValidation    ErrorCategory = "validation"
	ErrorCategoryProcessing    ErrorCategory = "processing"
	ErrorCategoryConfiguration ErrorCategory = "configuration"
	ErrorCategoryExternal      ErrorCategory = "external"
	ErrorCategoryUnknown       ErrorCategory = "unknown"
)

type ErrorHandler

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

ErrorHandler provides centralized error handling functionality

func NewErrorHandler

func NewErrorHandler(pluginName, ecosystem string) *ErrorHandler

NewErrorHandler creates a new error handler for a specific plugin/ecosystem

func (*ErrorHandler) HandleError

func (eh *ErrorHandler) HandleError(err error, stage string) *EcosystemError

HandleError processes an error and returns an appropriate EcosystemError

func (*ErrorHandler) WrapWithContext

func (eh *ErrorHandler) WrapWithContext(err error, message, stage string) *EcosystemError

WrapWithContext wraps an error with ecosystem context

type ErrorRecoveryStrategy

type ErrorRecoveryStrategy struct {
	MaxRetries      int           `json:"maxRetries"`
	RetryDelay      time.Duration `json:"retryDelay"`
	BackoffFactor   float64       `json:"backoffFactor"`
	RecoverableOnly bool          `json:"recoverableOnly"`
}

ErrorRecoveryStrategy defines how to handle different types of errors

func DefaultRecoveryStrategy

func DefaultRecoveryStrategy() ErrorRecoveryStrategy

DefaultRecoveryStrategy returns a sensible default recovery strategy

func (ErrorRecoveryStrategy) GetRetryDelay

func (strategy ErrorRecoveryStrategy) GetRetryDelay(attemptCount int) time.Duration

GetRetryDelay calculates the delay before the next retry attempt

func (ErrorRecoveryStrategy) ShouldRetry

func (strategy ErrorRecoveryStrategy) ShouldRetry(err *EcosystemError, attemptCount int) bool

ShouldRetry determines if an error should be retried based on the strategy

type ErrorSeverity

type ErrorSeverity string

ErrorSeverity defines the severity level of errors

const (
	ErrorSeverityLow      ErrorSeverity = "low"
	ErrorSeverityMedium   ErrorSeverity = "medium"
	ErrorSeverityHigh     ErrorSeverity = "high"
	ErrorSeverityCritical ErrorSeverity = "critical"
)

type GeneralConfig

type GeneralConfig struct {
	Environment string `json:"environment"`
	LogLevel    string `json:"logLevel"`
}

GeneralConfig holds general plugin configuration

type GenericResultMerger

type GenericResultMerger[T any] struct {
	// contains filtered or unexported fields
}

GenericResultMerger provides generic functionality for merging analysis results

func NewGenericResultMerger

func NewGenericResultMerger[T any](strategy MergeStrategy, mergeFunc func([]T) T) *GenericResultMerger[T]

NewGenericResultMerger creates a new generic result merger with a custom merge function

func (*GenericResultMerger[T]) GetMergeStrategy

func (m *GenericResultMerger[T]) GetMergeStrategy() string

GetMergeStrategy returns the merge strategy used by this merger

func (*GenericResultMerger[T]) MergeWorkspaces

func (m *GenericResultMerger[T]) MergeWorkspaces(results []T) T

MergeWorkspaces merges results from multiple language ecosystems

type GenericSBOMHandler

type GenericSBOMHandler struct {
	Analyzer SBOMAnalyzer
}

GenericSBOMHandler implements the AnalysisHandler interface for SBOM plugins

func (*GenericSBOMHandler) StartAnalysis

func (h *GenericSBOMHandler) StartAnalysis(
	databases *PluginDatabases,
	dispatcherMessage types_amqp.DispatcherPluginMessage,
	config plugin_db.Plugin,
	analysisDoc codeclarity.Analysis,
) (map[string]any, codeclarity.AnalysisStatus, error)

StartAnalysis implements the AnalysisHandler interface

type LicenseAnalysisStats

type LicenseAnalysisStats struct {
	NumberOfSpdxLicenses       int            `json:"numberOfSpdxLicenses"`
	NumberOfNonSpdxLicenses    int            `json:"numberOfNonSpdxLicenses"`
	NumberOfCopyLeftLicenses   int            `json:"numberOfCopyLeftLicenses"`
	NumberOfPermissiveLicenses int            `json:"numberOfPermissiveLicenses"`
	LicenseDist                map[string]int `json:"licenseDist"`
}

LicenseAnalysisStats represents license analysis statistics

type LicenseDependencyInfo

type LicenseDependencyInfo struct {
	Name              string   `json:"name"`
	Version           string   `json:"version"`
	Licenses          []string `json:"licenses"`
	LicenseExpression string   `json:"licenseExpression"`
	LicenseViolations []string `json:"licenseViolations"`
}

LicenseDependencyInfo represents dependency information for licenses

type LicenseOutput

type LicenseOutput struct {
	WorkSpaces     map[string]LicenseWorkspaceInfo `json:"workSpaces"`
	AnalysisStats  LicenseAnalysisStats            `json:"analysisStats"`
	AnalysisStatus string                          `json:"status"`
}

LicenseOutput represents the complete license analysis output

type LicenseResultMerger

type LicenseResultMerger struct {
	*GenericResultMerger[LicenseOutput]
	// contains filtered or unexported fields
}

LicenseResultMerger provides merge functionality for license analysis results

func NewLicenseResultMerger

func NewLicenseResultMerger(strategy MergeStrategy) *LicenseResultMerger

NewLicenseResultMerger creates a new license result merger

type LicenseWorkspaceInfo

type LicenseWorkspaceInfo struct {
	LicensesDepMap              map[string][]string              `json:"licensesDepMap"`
	NonSpdxLicensesDepMap       map[string][]string              `json:"nonSpdxLicensesDepMap"`
	LicenseComplianceViolations []string                         `json:"licenseComplianceViolations"`
	DependencyInfo              map[string]LicenseDependencyInfo `json:"dependencyInfo"`
}

LicenseWorkspaceInfo represents workspace license information in a generic way This mirrors the structure from the license plugin without importing it directly

type MergeStrategy

type MergeStrategy string

MergeStrategy defines different strategies for merging multi-language results This is a temporary definition until the ecosystem package is published remotely

const (
	MergeStrategyUnion        MergeStrategy = "union"        // Combine all results
	MergeStrategyIntersection MergeStrategy = "intersection" // Only results present in all languages
	MergeStrategyPriority     MergeStrategy = "priority"     // Prioritize results from specific languages
)

type MergerUtils

type MergerUtils struct{}

MergerUtils provides utility functions for common merge operations

func NewMergerUtils

func NewMergerUtils() *MergerUtils

NewMergerUtils creates a new instance of merger utilities

func (*MergerUtils) LogMergeOperation

func (u *MergerUtils) LogMergeOperation(operation string, inputCount int, outputDescription string)

LogMergeOperation logs information about a merge operation

func (*MergerUtils) MergeIntMaps

func (u *MergerUtils) MergeIntMaps(maps ...map[string]int) map[string]int

MergeIntMaps merges multiple int maps, summing values for duplicate keys

func (*MergerUtils) MergeStringMaps

func (u *MergerUtils) MergeStringMaps(maps ...map[string][]string) map[string][]string

MergeStringMaps merges multiple string maps, combining values for duplicate keys

func (*MergerUtils) MergeStringSlices

func (u *MergerUtils) MergeStringSlices(slices ...[]string) []string

MergeStringSlices merges multiple string slices, removing duplicates

func (*MergerUtils) ValidateNonEmptySlice

func (u *MergerUtils) ValidateNonEmptySlice(slice []interface{}, description string) bool

ValidateNonEmptySlice checks if a slice is not empty and logs a warning if it is

type PluginBase

type PluginBase struct {
	Config    plugin_db.Plugin
	DB        *PluginDatabases
	ConfigSvc *ConfigService
	Logger    *logrus.Logger
	// contains filtered or unexported fields
}

PluginBase provides common functionality for all plugins, eliminating boilerplate code

func CreatePluginBase

func CreatePluginBase() (*PluginBase, error)

CreatePluginBase creates a new PluginBase with all common setup handled

func (*PluginBase) Close

func (pb *PluginBase) Close() error

Close cleanly shuts down the plugin base and closes all connections

func (*PluginBase) Listen

func (pb *PluginBase) Listen(handler AnalysisHandler) error

Listen starts listening on the plugin's AMQP queue with automatic message handling

type PluginDatabases

type PluginDatabases struct {
	Codeclarity *bun.DB
	Knowledge   *bun.DB
	Plugins     *bun.DB
}

PluginDatabases holds all database connections needed by plugins

type QueueConfig

type QueueConfig struct {
	Name    string
	Durable bool
	Handler func(d amqp.Delivery)
}

type SBOMAnalyzer

type SBOMAnalyzer interface {
	// AnalyzeProject performs the SBOM analysis for a specific language
	AnalyzeProject(projectPath string, analysisId string, knowledgeDB interface{}) (SBOMOutput, error)

	// CanAnalyze checks if this analyzer can handle the given project
	CanAnalyze(projectPath string) bool

	// GetLanguage returns the language this analyzer handles
	GetLanguage() string

	// DetectFramework detects the framework used in the project (optional, can return empty string)
	DetectFramework(projectPath string) string

	// ConvertToMap converts the analyzer's output to map[string]any for storage
	ConvertToMap(output SBOMOutput) map[string]any

	// GetDependencyCount returns the total number of dependencies found
	GetDependencyCount(output SBOMOutput) int
}

SBOMAnalyzer defines the interface that all language-specific SBOM analyzers must implement

type SBOMOutput

type SBOMOutput interface {
	GetStatus() codeclarity.AnalysisStatus
	GetFramework() string
}

SBOMOutput represents the standard output structure for all SBOM analyzers

type ServiceBase

type ServiceBase struct {
	ConfigSvc *ConfigService
	DB        *ServiceDatabases

	Metrics     *ServiceMetrics
	ServiceName string
	Logger      *logrus.Logger
	// contains filtered or unexported fields
}

func CreateServiceBase

func CreateServiceBase() (*ServiceBase, error)

func CreateServiceBaseWithName

func CreateServiceBaseWithName(serviceName string) (*ServiceBase, error)

func (*ServiceBase) AddQueue

func (sb *ServiceBase) AddQueue(name string, durable bool, handler func(d amqp.Delivery))

func (*ServiceBase) Close

func (sb *ServiceBase) Close() error

func (*ServiceBase) GetHealthStatus

func (sb *ServiceBase) GetHealthStatus() map[string]any

GetHealthStatus returns the current health status of the service

func (*ServiceBase) LogDebug

func (sb *ServiceBase) LogDebug(message string, fields logrus.Fields)

func (*ServiceBase) LogError

func (sb *ServiceBase) LogError(message string, err error, fields logrus.Fields)

func (*ServiceBase) LogInfo

func (sb *ServiceBase) LogInfo(message string, fields logrus.Fields)

Structured logging convenience methods

func (*ServiceBase) LogWarn

func (sb *ServiceBase) LogWarn(message string, fields logrus.Fields)

func (*ServiceBase) SendMessage

func (sb *ServiceBase) SendMessage(queueName string, data []byte) error

func (*ServiceBase) StartListening

func (sb *ServiceBase) StartListening() error

func (*ServiceBase) WaitForever

func (sb *ServiceBase) WaitForever()

type ServiceConfig

type ServiceConfig struct {
	Name      string `json:"name"`
	Version   string `json:"version"`
	ImageName string `json:"image_name"`
}

ServiceConfig represents the service configuration from config.json

type ServiceDatabases

type ServiceDatabases struct {
	CodeClarity *bun.DB
	Knowledge   *bun.DB
	Plugins     *bun.DB
	Config      *bun.DB
}

type ServiceMetrics

type ServiceMetrics struct {
	// Service health metrics
	ServiceHealthStatus *prometheus.GaugeVec
	ServiceUptime       prometheus.Gauge
	ServiceRestarts     prometheus.Counter

	// Database metrics
	DatabaseConnections   *prometheus.GaugeVec
	DatabaseOperations    *prometheus.CounterVec
	DatabaseQueryDuration *prometheus.HistogramVec
	DatabaseHealthChecks  *prometheus.CounterVec

	// AMQP metrics
	AMQPConnections       *prometheus.GaugeVec
	MessagesProcessed     *prometheus.CounterVec
	MessageProcessingTime *prometheus.HistogramVec
	QueueConsumers        *prometheus.GaugeVec
	// contains filtered or unexported fields
}

ServiceMetrics holds all Prometheus metrics for a service

func CreateServiceMetrics

func CreateServiceMetrics(serviceName string) *ServiceMetrics

CreateServiceMetrics creates and registers Prometheus metrics for a service

func (*ServiceMetrics) RecordDatabaseHealthCheck

func (m *ServiceMetrics) RecordDatabaseHealthCheck(database, status string)

RecordDatabaseHealthCheck records a database health check

func (*ServiceMetrics) RecordDatabaseOperation

func (m *ServiceMetrics) RecordDatabaseOperation(database, operation, status string, duration time.Duration)

RecordDatabaseOperation records a database operation

func (*ServiceMetrics) RecordMessageProcessed

func (m *ServiceMetrics) RecordMessageProcessed(queue, status string, duration time.Duration)

RecordMessageProcessed records an AMQP message processing event

func (*ServiceMetrics) RecordRestart

func (m *ServiceMetrics) RecordRestart()

RecordRestart increments the restart counter

func (*ServiceMetrics) SetAMQPConnections

func (m *ServiceMetrics) SetAMQPConnections(connectionType string, count int)

SetAMQPConnections sets the number of AMQP connections

func (*ServiceMetrics) SetHealthStatus

func (m *ServiceMetrics) SetHealthStatus(serviceName, component string, healthy bool)

SetHealthStatus sets the health status for a component

func (*ServiceMetrics) SetQueueConsumers

func (m *ServiceMetrics) SetQueueConsumers(queue string, count int)

SetQueueConsumers sets the number of consumers for a queue

func (*ServiceMetrics) UpdateUptime

func (m *ServiceMetrics) UpdateUptime()

UpdateUptime updates the service uptime metric

type VulnerabilityInfo

type VulnerabilityInfo struct {
	Id              string   `json:"id"`
	PackageName     string   `json:"packageName"`
	VersionAffected string   `json:"versionAffected"`
	Severity        string   `json:"severity"`
	Score           float64  `json:"score"`
	Description     string   `json:"description"`
	References      []string `json:"references"`
	FixedVersions   []string `json:"fixedVersions"`
	Ecosystem       string   `json:"ecosystem"`
	Source          string   `json:"source"` // OSV, NVD, etc.
}

VulnerabilityInfo represents vulnerability information in a generic way

type VulnerabilityOutput

type VulnerabilityOutput struct {
	WorkSpaces     map[string]VulnerabilityWorkspaceInfo `json:"workSpaces"`
	AnalysisStatus string                                `json:"status"`
}

VulnerabilityOutput represents the complete vulnerability analysis output

type VulnerabilityResultMerger

type VulnerabilityResultMerger struct {
	*GenericResultMerger[VulnerabilityOutput]
	// contains filtered or unexported fields
}

VulnerabilityResultMerger provides merge functionality for vulnerability analysis results

func NewVulnerabilityResultMerger

func NewVulnerabilityResultMerger(strategy MergeStrategy) *VulnerabilityResultMerger

NewVulnerabilityResultMerger creates a new vulnerability result merger

type VulnerabilityStats

type VulnerabilityStats struct {
	Total              int             `json:"total"`
	BySeverity         map[string]int  `json:"bySeverity"`
	ByEcosystem        map[string]int  `json:"byEcosystem"`
	BySource           map[string]int  `json:"bySource"`
	UniquePackages     map[string]bool `json:"-"` // Internal use only
	UniquePackageCount int             `json:"uniquePackageCount"`
}

VulnerabilityStats represents calculated vulnerability statistics

type VulnerabilityStatsCalculator

type VulnerabilityStatsCalculator struct{}

VulnerabilityStatsCalculator provides utilities for calculating vulnerability statistics

func NewVulnerabilityStatsCalculator

func NewVulnerabilityStatsCalculator() *VulnerabilityStatsCalculator

NewVulnerabilityStatsCalculator creates a new stats calculator

func (*VulnerabilityStatsCalculator) CalculateStats

CalculateStats calculates vulnerability statistics from merged results

type VulnerabilityWorkspaceInfo

type VulnerabilityWorkspaceInfo struct {
	Vulnerabilities []VulnerabilityInfo `json:"vulnerabilities"`
}

VulnerabilityWorkspaceInfo represents workspace vulnerability information

type WorkspaceKey

type WorkspaceKey struct {
	Name      string
	Ecosystem string
}

WorkspaceKey represents a unique identifier for a workspace across multiple ecosystems

func (WorkspaceKey) String

func (wk WorkspaceKey) String() string

String returns the string representation of the workspace key

Jump to

Keyboard shortcuts

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