mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
320 lines
8.4 KiB
Go
320 lines
8.4 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package cargo
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"path"
|
|
"strconv"
|
|
|
|
packages_model "github.com/hanzoai/git/models/packages"
|
|
repo_model "github.com/hanzoai/git/models/repo"
|
|
user_model "github.com/hanzoai/git/models/user"
|
|
"github.com/hanzoai/git/modules/git"
|
|
"github.com/hanzoai/git/modules/json"
|
|
cargo_module "github.com/hanzoai/git/modules/packages/cargo"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
"github.com/hanzoai/git/modules/structs"
|
|
"github.com/hanzoai/git/modules/util"
|
|
repo_service "github.com/hanzoai/git/services/repository"
|
|
files_service "github.com/hanzoai/git/services/repository/files"
|
|
)
|
|
|
|
const (
|
|
IndexRepositoryName = "_cargo-index"
|
|
ConfigFileName = "config.json"
|
|
)
|
|
|
|
// https://doc.rust-lang.org/cargo/reference/registries.html#index-format
|
|
|
|
func BuildPackagePath(name string) string {
|
|
switch len(name) {
|
|
case 0:
|
|
panic("Cargo package name can not be empty")
|
|
case 1:
|
|
return path.Join("1", name)
|
|
case 2:
|
|
return path.Join("2", name)
|
|
case 3:
|
|
return path.Join("3", string(name[0]), name)
|
|
default:
|
|
return path.Join(name[0:2], name[2:4], name)
|
|
}
|
|
}
|
|
|
|
func InitializeIndexRepository(ctx context.Context, doer, owner *user_model.User) error {
|
|
repo, err := getOrCreateIndexRepository(ctx, doer, owner)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := createOrUpdateConfigFile(ctx, repo, doer, owner); err != nil {
|
|
return fmt.Errorf("createOrUpdateConfigFile: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func RebuildIndex(ctx context.Context, doer, owner *user_model.User) error {
|
|
repo, err := getOrCreateIndexRepository(ctx, doer, owner)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ps, err := packages_model.GetPackagesByType(ctx, owner.ID, packages_model.TypeCargo)
|
|
if err != nil {
|
|
return fmt.Errorf("GetPackagesByType: %w", err)
|
|
}
|
|
|
|
return alterRepositoryContent(
|
|
ctx,
|
|
doer,
|
|
repo,
|
|
"Rebuild Cargo Index",
|
|
func(t *files_service.TemporaryUploadRepository) error {
|
|
// Remove all existing content but the Cargo config
|
|
files, err := t.LsFiles(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i, file := range files {
|
|
if file == ConfigFileName {
|
|
files[i] = files[len(files)-1]
|
|
files = files[:len(files)-1]
|
|
break
|
|
}
|
|
}
|
|
if err := t.RemoveFilesFromIndex(ctx, files...); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add all packages
|
|
for _, p := range ps {
|
|
if err := addOrUpdatePackageIndex(ctx, t, p); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
}
|
|
|
|
func UpdatePackageIndexIfExists(ctx context.Context, doer, owner *user_model.User, packageID int64) error {
|
|
// We do not want to force the creation of the repo here
|
|
// cargo http index does not rely on the repo itself,
|
|
// so if the repo does not exist, we just do nothing.
|
|
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner.Name, IndexRepositoryName)
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrNotExist) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("GetRepositoryByOwnerAndName: %w", err)
|
|
}
|
|
|
|
p, err := packages_model.GetPackageByID(ctx, packageID)
|
|
if err != nil {
|
|
return fmt.Errorf("GetPackageByID[%d]: %w", packageID, err)
|
|
}
|
|
|
|
return alterRepositoryContent(
|
|
ctx,
|
|
doer,
|
|
repo,
|
|
"Update "+p.Name,
|
|
func(t *files_service.TemporaryUploadRepository) error {
|
|
return addOrUpdatePackageIndex(ctx, t, p)
|
|
},
|
|
)
|
|
}
|
|
|
|
type IndexVersionEntry struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"vers"`
|
|
Dependencies []*cargo_module.Dependency `json:"deps"`
|
|
FileChecksum string `json:"cksum"`
|
|
Features map[string][]string `json:"features"`
|
|
Yanked bool `json:"yanked"`
|
|
Links string `json:"links,omitempty"`
|
|
}
|
|
|
|
func BuildPackageIndex(ctx context.Context, p *packages_model.Package) (*bytes.Buffer, error) {
|
|
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
|
PackageID: p.ID,
|
|
Sort: packages_model.SortVersionAsc,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("SearchVersions[%s]: %w", p.Name, err)
|
|
}
|
|
if len(pvs) == 0 {
|
|
return nil, nil //nolint:nilnil // return nil to indicate that the package has no versions
|
|
}
|
|
|
|
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GetPackageDescriptors[%s]: %w", p.Name, err)
|
|
}
|
|
|
|
var b bytes.Buffer
|
|
for _, pd := range pds {
|
|
metadata := pd.Metadata.(*cargo_module.Metadata)
|
|
|
|
dependencies := metadata.Dependencies
|
|
if dependencies == nil {
|
|
dependencies = make([]*cargo_module.Dependency, 0)
|
|
}
|
|
|
|
features := metadata.Features
|
|
if features == nil {
|
|
features = make(map[string][]string)
|
|
}
|
|
|
|
yanked, _ := strconv.ParseBool(pd.VersionProperties.GetByName(cargo_module.PropertyYanked))
|
|
entry, err := json.Marshal(&IndexVersionEntry{
|
|
Name: pd.Package.Name,
|
|
Version: pd.Version.Version,
|
|
Dependencies: dependencies,
|
|
FileChecksum: pd.Files[0].Blob.HashSHA256,
|
|
Features: features,
|
|
Yanked: yanked,
|
|
Links: metadata.Links,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b.Write(entry)
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
return &b, nil
|
|
}
|
|
|
|
func addOrUpdatePackageIndex(ctx context.Context, t *files_service.TemporaryUploadRepository, p *packages_model.Package) error {
|
|
b, err := BuildPackageIndex(ctx, p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if b == nil {
|
|
return nil
|
|
}
|
|
|
|
return writeObjectToIndex(ctx, t, BuildPackagePath(p.LowerName), b)
|
|
}
|
|
|
|
func getOrCreateIndexRepository(ctx context.Context, doer, owner *user_model.User) (*repo_model.Repository, error) {
|
|
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner.Name, IndexRepositoryName)
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrNotExist) {
|
|
repo, err = repo_service.CreateRepositoryDirectly(ctx, doer, owner, repo_service.CreateRepoOptions{
|
|
Name: IndexRepositoryName,
|
|
}, true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("CreateRepository: %w", err)
|
|
}
|
|
} else {
|
|
return nil, fmt.Errorf("GetRepositoryByOwnerAndName: %w", err)
|
|
}
|
|
}
|
|
|
|
return repo, nil
|
|
}
|
|
|
|
type Config struct {
|
|
DownloadURL string `json:"dl"`
|
|
APIURL string `json:"api"`
|
|
AuthRequired bool `json:"auth-required"`
|
|
}
|
|
|
|
func BuildConfig(owner *user_model.User, isPrivate bool) *Config {
|
|
return &Config{
|
|
DownloadURL: setting.PackageRegistryURL(owner.Name, "cargo") + "/api/v1/crates",
|
|
APIURL: setting.PackageRegistryURL(owner.Name, "cargo"),
|
|
AuthRequired: isPrivate,
|
|
}
|
|
}
|
|
|
|
func createOrUpdateConfigFile(ctx context.Context, repo *repo_model.Repository, doer, owner *user_model.User) error {
|
|
return alterRepositoryContent(
|
|
ctx,
|
|
doer,
|
|
repo,
|
|
"Initialize Cargo Config",
|
|
func(t *files_service.TemporaryUploadRepository) error {
|
|
var b bytes.Buffer
|
|
err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInViewStrict || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return writeObjectToIndex(ctx, t, ConfigFileName, &b)
|
|
},
|
|
)
|
|
}
|
|
|
|
// This is a shorter version of CreateOrUpdateRepoFile which allows to perform multiple actions on a git repository
|
|
func alterRepositoryContent(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commitMessage string, fn func(*files_service.TemporaryUploadRepository) error) error {
|
|
t, err := files_service.NewTemporaryUploadRepository(repo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer t.Close()
|
|
|
|
var lastCommitID string
|
|
if err := t.Clone(ctx, repo.DefaultBranch, true); err != nil {
|
|
if !git.IsErrBranchNotExist(err) || !repo.IsEmpty {
|
|
return err
|
|
}
|
|
if err := t.Init(ctx, repo.ObjectFormatName); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := t.SetDefaultIndex(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
commit, err := t.GetBranchCommit(repo.DefaultBranch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
lastCommitID = commit.ID.String()
|
|
}
|
|
|
|
if err := fn(t); err != nil {
|
|
return err
|
|
}
|
|
|
|
treeHash, err := t.WriteTree(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
commitOpts := &files_service.CommitTreeUserOptions{
|
|
ParentCommitID: lastCommitID,
|
|
TreeHash: treeHash,
|
|
CommitMessage: commitMessage,
|
|
DoerUser: doer,
|
|
}
|
|
commitHash, err := t.CommitTree(ctx, commitOpts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return t.Push(ctx, doer, commitHash, repo.DefaultBranch, false)
|
|
}
|
|
|
|
func writeObjectToIndex(ctx context.Context, t *files_service.TemporaryUploadRepository, path string, r io.Reader) error {
|
|
hash, err := t.HashObjectAndWrite(ctx, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return t.AddObjectToIndex(ctx, "100644", hash, path)
|
|
}
|