aboutsummaryrefslogtreecommitdiff
path: root/internal/modules/git_ops.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/modules/git_ops.go')
-rw-r--r--internal/modules/git_ops.go270
1 files changed, 270 insertions, 0 deletions
diff --git a/internal/modules/git_ops.go b/internal/modules/git_ops.go
new file mode 100644
index 0000000..5fd37c7
--- /dev/null
+++ b/internal/modules/git_ops.go
@@ -0,0 +1,270 @@
+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
+}