mirror of
https://github.com/vee1e/gittuf.git
synced 2026-09-01 10:18:18 +00:00
Restructure storage so gittuf verification can run over backends other
than the git binary (e.g. go-git):
- pkg/githash: concrete Git object hash, stdlib-only.
gitinterface.Hash aliases it.
- pkg/gitstore: the single Storer interface (24 methods) that all
storage consumers program against, plus the shared
ErrReferenceNotFound sentinel. *gitinterface.Repository satisfies it
structurally (compile-time asserted). Also defines ConfigKey, the
canonical type for the Git config settings gittuf reads.
- pkg/rsl (from internal/rsl): entry model, codec, and readers over
gitstore.Storer; zero gitinterface/sigstore dependencies. rsl.Hash
aliases githash.Hash; nil is the unset-Hash sentinel and IsZero
matches nil and empty as well as both format zeros (no
object-format-unaware ZeroHash). Entry commits (empty tree on the
RSL ref) are owned by the package; no storer adapter.
- internal/signerverifier/gitobject: verifies commit/tag signatures
over (payload, signature) bytes, Rekor URL as an option. The storage
half is Repository.GetObjectSignature. Removes sigstore, cosign, and
gitsign from gitinterface's dependency tree.
- internal/propagation: propagation workflow, moved off pkg/rsl's
public API (its tuf directive types are internal).
- internal/{attestations,cache,policy}: storage via gitstore.Storer;
tree writing via WriteTree(blobs, subtrees).
Breaking changes to pkg/gitinterface: Repository.VerifySignature and
the verification sentinels are removed (use gitobject.Verify);
ErrReferenceNotFound now aliases gitstore's. Repository.GetGitConfig
(which returned the whole config map) is replaced by
LookupConfig(gitstore.ConfigKey), returning a single setting's value.
Policy resolves the Rekor override from git config once per
verification and extracts signed payloads once per object instead of
per key attempt.
Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paulo Gomes <paulo@entire.io>
248 lines
6.9 KiB
Go
248 lines
6.9 KiB
Go
// Copyright The gittuf Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package gitinterface
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/gittuf/gittuf/pkg/gitstore"
|
|
)
|
|
|
|
const (
|
|
RefPrefix = "refs/"
|
|
BranchRefPrefix = "refs/heads/"
|
|
TagRefPrefix = "refs/tags/"
|
|
RemoteRefPrefix = "refs/remotes/"
|
|
)
|
|
|
|
var ErrReferenceNotFound = gitstore.ErrReferenceNotFound
|
|
|
|
// GetReference returns the tip of the specified Git reference.
|
|
func (r *Repository) GetReference(refName string) (Hash, error) {
|
|
refTipID, err := r.executor("rev-parse", refName).executeString()
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "unknown revision or path not in the working tree") {
|
|
return r.ZeroHash(), ErrReferenceNotFound
|
|
}
|
|
return r.ZeroHash(), fmt.Errorf("unable to read reference '%s': %w", refName, err)
|
|
}
|
|
|
|
hash, err := NewHash(refTipID)
|
|
if err != nil {
|
|
return r.ZeroHash(), fmt.Errorf("invalid Git ID for reference '%s': %w", refName, err)
|
|
}
|
|
|
|
return hash, nil
|
|
}
|
|
|
|
// SetReference sets the specified reference to the provided Git ID.
|
|
func (r *Repository) SetReference(refName string, gitID Hash) error {
|
|
_, err := r.executor("update-ref", "--create-reflog", refName, gitID.String()).executeString()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to set Git reference '%s' to '%s': %w", refName, gitID.String(), err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteReference deletes the specified Git reference.
|
|
func (r *Repository) DeleteReference(refName string) error {
|
|
_, err := r.executor("update-ref", "-d", refName).executeString()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to delete Git reference '%s': %w", refName, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CheckAndSetReference sets the specified reference to the provided Git ID if
|
|
// the reference is currently set to `oldGitID`.
|
|
func (r *Repository) CheckAndSetReference(refName string, newGitID, oldGitID Hash) error {
|
|
_, err := r.executor("update-ref", "--create-reflog", refName, newGitID.String(), oldGitID.String()).executeString()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to set Git reference '%s' to '%s': %w", refName, newGitID.String(), err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetSymbolicReferenceTarget returns the name of the Git reference the provided
|
|
// symbolic Git reference is pointing to.
|
|
func (r *Repository) GetSymbolicReferenceTarget(refName string) (string, error) {
|
|
symTarget, err := r.executor("symbolic-ref", refName).executeString()
|
|
if err != nil {
|
|
return "", fmt.Errorf("unable to resolve %s: %w", refName, err)
|
|
}
|
|
|
|
return symTarget, nil
|
|
}
|
|
|
|
// SetSymbolicReference sets the specified symbolic reference to the specified
|
|
// target reference.
|
|
func (r *Repository) SetSymbolicReference(symRefName, targetRefName string) error {
|
|
_, err := r.executor("symbolic-ref", symRefName, targetRefName).executeString()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to set symbolic Git reference '%s' to '%s': %w", symRefName, targetRefName, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AbsoluteReference returns the fully qualified reference path for the provided
|
|
// Git ref.
|
|
// Source: https://git-scm.com/docs/gitrevisions#Documentation/gitrevisions.txt-emltrefnamegtemegemmasterememheadsmasterememrefsheadsmasterem
|
|
func (r *Repository) AbsoluteReference(target string) (string, error) {
|
|
_, err := os.Stat(path.Join(r.gitDirPath, target))
|
|
if err == nil {
|
|
if strings.HasPrefix(target, RefPrefix) {
|
|
// not symbolic ref
|
|
return target, nil
|
|
}
|
|
// symbolic ref such as .git/HEAD
|
|
return r.GetSymbolicReferenceTarget(target)
|
|
}
|
|
|
|
// We may have a ref that isn't available locally but is still ref-prefixed.
|
|
if strings.HasPrefix(target, RefPrefix) {
|
|
return target, nil
|
|
}
|
|
|
|
// If target is a full ref already and it's stored in the GIT_DIR/refs
|
|
// directory, we don't reach this point. Below, we handle cases where the
|
|
// ref may be packed.
|
|
|
|
// Check if custom reference
|
|
customName := CustomReferenceName(target)
|
|
_, err = r.GetReference(customName)
|
|
if err == nil {
|
|
return customName, nil
|
|
}
|
|
if !errors.Is(err, ErrReferenceNotFound) {
|
|
return "", err
|
|
}
|
|
|
|
// Check if tag
|
|
tagName := TagReferenceName(target)
|
|
_, err = r.GetReference(tagName)
|
|
if err == nil {
|
|
return tagName, nil
|
|
}
|
|
if !errors.Is(err, ErrReferenceNotFound) {
|
|
return "", err
|
|
}
|
|
|
|
// Check if branch
|
|
branchName := BranchReferenceName(target)
|
|
_, err = r.GetReference(branchName)
|
|
if err == nil {
|
|
return branchName, nil
|
|
}
|
|
if !errors.Is(err, ErrReferenceNotFound) {
|
|
return "", err
|
|
}
|
|
|
|
// Check if remote tracker ref
|
|
remoteRefName := RemoteReferenceName(target)
|
|
_, err = r.GetReference(remoteRefName)
|
|
if err == nil {
|
|
return remoteRefName, nil
|
|
}
|
|
if !errors.Is(err, ErrReferenceNotFound) {
|
|
return "", err
|
|
}
|
|
|
|
remoteRefHEAD := path.Join(remoteRefName, "HEAD")
|
|
_, err = r.GetReference(remoteRefHEAD)
|
|
if err == nil {
|
|
return remoteRefHEAD, nil
|
|
}
|
|
if !errors.Is(err, ErrReferenceNotFound) {
|
|
return "", err
|
|
}
|
|
|
|
return "", ErrReferenceNotFound
|
|
}
|
|
|
|
// RefSpec creates a Git refspec for the specified ref. For more information on
|
|
// the Git refspec, please consult:
|
|
// https://git-scm.com/book/en/v2/Git-Internals-The-Refspec.
|
|
func (r *Repository) RefSpec(refName, remoteName string, fastForwardOnly bool) (string, error) {
|
|
var (
|
|
refPath string
|
|
err error
|
|
)
|
|
|
|
refPath = refName
|
|
if !strings.HasPrefix(refPath, RefPrefix) {
|
|
refPath, err = r.AbsoluteReference(refName)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
if strings.HasPrefix(refPath, TagRefPrefix) {
|
|
// TODO: check if this is correct, AFAICT tags aren't tracked in the
|
|
// remotes namespace.
|
|
fastForwardOnly = true
|
|
}
|
|
|
|
// local is always refPath, destination depends on remoteName
|
|
localPath := refPath
|
|
var remotePath string
|
|
if len(remoteName) > 0 {
|
|
remotePath = RemoteRef(refPath, remoteName)
|
|
} else {
|
|
remotePath = refPath
|
|
}
|
|
|
|
refSpecString := fmt.Sprintf("%s:%s", localPath, remotePath)
|
|
if !fastForwardOnly {
|
|
refSpecString = fmt.Sprintf("+%s", refSpecString)
|
|
}
|
|
|
|
return refSpecString, nil
|
|
}
|
|
|
|
// CustomReferenceName returns the full reference name in the form
|
|
// `refs/<customName>`.
|
|
func CustomReferenceName(customName string) string {
|
|
if strings.HasPrefix(customName, RefPrefix) {
|
|
return customName
|
|
}
|
|
|
|
return fmt.Sprintf("%s%s", RefPrefix, customName)
|
|
}
|
|
|
|
// TagReferenceName returns the full reference name for the specified tag in the
|
|
// form `refs/tags/<tagName>`.
|
|
func TagReferenceName(tagName string) string {
|
|
if strings.HasPrefix(tagName, TagRefPrefix) {
|
|
return tagName
|
|
}
|
|
|
|
return fmt.Sprintf("%s%s", TagRefPrefix, tagName)
|
|
}
|
|
|
|
// BranchReferenceName returns the full reference name for the specified branch
|
|
// in the form `refs/heads/<branchName>`.
|
|
func BranchReferenceName(branchName string) string {
|
|
if strings.HasPrefix(branchName, BranchRefPrefix) {
|
|
return branchName
|
|
}
|
|
|
|
return fmt.Sprintf("%s%s", BranchRefPrefix, branchName)
|
|
}
|
|
|
|
// RemoteReferenceName returns the full reference name in the form
|
|
// `refs/remotes/<name>`.
|
|
func RemoteReferenceName(name string) string {
|
|
if strings.HasPrefix(name, RemoteRefPrefix) {
|
|
return name
|
|
}
|
|
|
|
return fmt.Sprintf("%s%s", RemoteRefPrefix, name)
|
|
}
|