package modules import ( "archive/zip" "bytes" "fmt" "io" "path/filepath" "sort" "time" "github.com/Masterminds/semver/v3" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" ) // getVersions returns all available versions (tags) for a repository func (h *ModuleHandler) getVersions(repoPath string) ([]string, error) { repo, err := git.PlainOpen(repoPath) if err != nil { return nil, fmt.Errorf("failed to open repository: %w", err) } tagRefs, err := repo.Tags() if err != nil { return nil, fmt.Errorf("failed to get tags: %w", err) } semverVersions := semver.Collection{} err = tagRefs.ForEach(func(ref *plumbing.Reference) error { tagName := ref.Name().Short() // Only include semantic version tags if version, err := semver.NewVersion(tagName); err == nil { semverVersions = append(semverVersions, version) } return nil }) if err != nil { return nil, fmt.Errorf("failed to iterate tags: %w", err) } // Sort versions in descending order (newest first) sort.Sort(sort.Reverse(semverVersions)) // Convert back to strings with v prefix versions := make([]string, len(semverVersions)) for i, v := range semverVersions { versions[i] = "v" + v.String() } return versions, nil } // getLatestVersion returns the latest version tag or a pseudo-version func (h *ModuleHandler) getLatestVersion(repoPath string) (string, error) { versions, err := h.getVersions(repoPath) if err != nil { return "", err } if len(versions) > 0 { return versions[0], nil } // No tags found, generate pseudo-version from latest commit return h.generatePseudoVersion(repoPath) } // generatePseudoVersion creates a pseudo-version from the latest commit func (h *ModuleHandler) generatePseudoVersion(repoPath string) (string, error) { repo, err := git.PlainOpen(repoPath) if err != nil { return "", fmt.Errorf("failed to open repository: %w", err) } head, err := repo.Head() if err != nil { return "", fmt.Errorf("failed to get HEAD: %w", err) } commit, err := repo.CommitObject(head.Hash()) if err != nil { return "", fmt.Errorf("failed to get commit: %w", err) } // Format: v0.0.0-yyyymmddhhmmss-abcdefabcdef timestamp := commit.Committer.When.UTC().Format("20060102150405") shortHash := head.Hash().String()[:12] return fmt.Sprintf("v0.0.0-%s-%s", timestamp, shortHash), nil } // getVersionTimestamp returns the timestamp for a specific version func (h *ModuleHandler) getVersionTimestamp(repoPath, version string) (string, error) { repo, err := git.PlainOpen(repoPath) if err != nil { return "", fmt.Errorf("failed to open repository: %w", err) } // Try to resolve as tag first tagRef, err := repo.Tag(version) if err != nil && len(version) > 0 && version[0] == 'v' { // Try without v prefix tagRef, err = repo.Tag(version[1:]) } if err == nil { // Try to get commit directly first (lightweight tag) hash := tagRef.Hash() if hash.IsZero() { return "", fmt.Errorf("tag reference has zero hash for version %s", version) } commit, err := repo.CommitObject(hash) if err == nil && commit != nil { return commit.Committer.When.UTC().Format(time.RFC3339), nil } // Try as annotated tag tagObj, err := repo.TagObject(hash) if err == nil && tagObj != nil { return tagObj.Tagger.When.UTC().Format(time.RFC3339), nil } return "", fmt.Errorf("failed to get timestamp for tag %s: %w", version, err) } // Try to resolve as commit hash hash := plumbing.NewHash(version) commit, err := repo.CommitObject(hash) if err != nil { return "", fmt.Errorf("version not found: %s", version) } return commit.Committer.When.UTC().Format(time.RFC3339), nil } // getModFile returns the go.mod file content for a specific version func (h *ModuleHandler) getModFile(repoPath, version string) ([]byte, error) { repo, err := git.PlainOpen(repoPath) if err != nil { return nil, fmt.Errorf("failed to open repository: %w", err) } hash, err := h.resolveVersion(repo, version) if err != nil { return nil, err } commit, err := repo.CommitObject(hash) if err != nil { return nil, fmt.Errorf("failed to get commit: %w", err) } tree, err := commit.Tree() if err != nil { return nil, fmt.Errorf("failed to get tree: %w", err) } file, err := tree.File("go.mod") if err != nil { return nil, fmt.Errorf("go.mod not found: %w", err) } content, err := file.Contents() if err != nil { return nil, fmt.Errorf("failed to read go.mod: %w", err) } return []byte(content), nil } // getModuleZip creates a zip archive of the module source for a specific version func (h *ModuleHandler) getModuleZip(repoPath, version string) ([]byte, error) { repo, err := git.PlainOpen(repoPath) if err != nil { return nil, fmt.Errorf("failed to open repository: %w", err) } hash, err := h.resolveVersion(repo, version) if err != nil { return nil, err } commit, err := repo.CommitObject(hash) if err != nil { return nil, fmt.Errorf("failed to get commit: %w", err) } tree, err := commit.Tree() if err != nil { return nil, fmt.Errorf("failed to get tree: %w", err) } buf := bytes.Buffer{} zipWriter := zip.NewWriter(&buf) // Walk the tree and add files to zip err = tree.Files().ForEach(func(file *object.File) error { // Skip files in nested modules (subdirectories with go.mod) if h.isInNestedModule(tree, filepath.Dir(file.Name)) && file.Name != "go.mod" { return nil } fileWriter, err := zipWriter.Create(file.Name) if err != nil { return err } reader, err := file.Reader() if err != nil { return err } defer reader.Close() _, err = io.Copy(fileWriter, reader) return err }) if err != nil { return nil, fmt.Errorf("failed to create zip: %w", err) } err = zipWriter.Close() if err != nil { return nil, fmt.Errorf("failed to close zip: %w", err) } return buf.Bytes(), nil } // resolveVersion converts a version string to a commit hash func (h *ModuleHandler) resolveVersion(repo *git.Repository, version string) (plumbing.Hash, error) { // Try as tag first tagRef, err := repo.Tag(version) if err == nil { return tagRef.Hash(), nil } // Try without v prefix if version starts with v if len(version) > 0 && version[0] == 'v' { tagRef, err := repo.Tag(version[1:]) if err == nil { return tagRef.Hash(), nil } } // Try as commit hash if len(version) >= 7 { hash := plumbing.NewHash(version) _, err := repo.CommitObject(hash) if err == nil { return hash, nil } } return plumbing.ZeroHash, fmt.Errorf("version not found: %s", version) } // isInNestedModule checks if a directory contains a go.mod file (nested module) func (h *ModuleHandler) isInNestedModule(tree *object.Tree, dir string) bool { if dir == "" || dir == "." { return false } modPath := filepath.Join(dir, "go.mod") _, err := tree.File(modPath) return err == nil }