diff options
Diffstat (limited to 'internal/modules/git_ops.go')
| -rw-r--r-- | internal/modules/git_ops.go | 270 |
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 +} |