feat(pkg): support install refs in shorthand
Some checks are pending
Security Scan / security (push) Waiting to run
Some checks are pending
Security Scan / security (push) Waiting to run
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
27e44f069a
commit
fcadba08b1
4 changed files with 132 additions and 12 deletions
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
@ -22,10 +23,12 @@ var (
|
||||||
installAddToReg bool
|
installAddToReg bool
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var errInvalidPkgInstallSource = errors.New("invalid repo format: use org/repo or org/repo@ref")
|
||||||
|
|
||||||
// addPkgInstallCommand adds the 'pkg install' command.
|
// addPkgInstallCommand adds the 'pkg install' command.
|
||||||
func addPkgInstallCommand(parent *cobra.Command) {
|
func addPkgInstallCommand(parent *cobra.Command) {
|
||||||
installCmd := &cobra.Command{
|
installCmd := &cobra.Command{
|
||||||
Use: "install [org/]repo",
|
Use: "install [org/]repo[@ref]",
|
||||||
Short: i18n.T("cmd.pkg.install.short"),
|
Short: i18n.T("cmd.pkg.install.short"),
|
||||||
Long: i18n.T("cmd.pkg.install.long"),
|
Long: i18n.T("cmd.pkg.install.long"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
@ -48,14 +51,9 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
|
||||||
// Parse repo shorthand:
|
// Parse repo shorthand:
|
||||||
// - repoName -> defaults to host-uk/repoName
|
// - repoName -> defaults to host-uk/repoName
|
||||||
// - org/repo -> uses the explicit org
|
// - org/repo -> uses the explicit org
|
||||||
org := "host-uk"
|
org, repoName, ref, err := parsePkgInstallSource(repoArg)
|
||||||
repoName := repoArg
|
if err != nil {
|
||||||
if strings.Contains(repoArg, "/") {
|
return err
|
||||||
parts := strings.Split(repoArg, "/")
|
|
||||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
|
||||||
return errors.New(i18n.T("cmd.pkg.error.invalid_repo_format"))
|
|
||||||
}
|
|
||||||
org, repoName = parts[0], parts[1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine target directory
|
// Determine target directory
|
||||||
|
|
@ -93,11 +91,18 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s/%s\n", dimStyle.Render(i18n.T("cmd.pkg.install.installing_label")), org, repoName)
|
fmt.Printf("%s %s/%s\n", dimStyle.Render(i18n.T("cmd.pkg.install.installing_label")), org, repoName)
|
||||||
|
if ref != "" {
|
||||||
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("ref")), ref)
|
||||||
|
}
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("target")), repoPath)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("target")), repoPath)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
fmt.Printf(" %s... ", dimStyle.Render(i18n.T("common.status.cloning")))
|
fmt.Printf(" %s... ", dimStyle.Render(i18n.T("common.status.cloning")))
|
||||||
err := gitClone(ctx, org, repoName, repoPath)
|
if ref == "" {
|
||||||
|
err = gitClone(ctx, org, repoName, repoPath)
|
||||||
|
} else {
|
||||||
|
err = gitCloneRef(ctx, org, repoName, repoPath, ref)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%s\n", errorStyle.Render("✗ "+err.Error()))
|
fmt.Printf("%s\n", errorStyle.Render("✗ "+err.Error()))
|
||||||
return err
|
return err
|
||||||
|
|
@ -118,6 +123,36 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parsePkgInstallSource(repoArg string) (org, repoName, ref string, err error) {
|
||||||
|
org = "host-uk"
|
||||||
|
repoName = strings.TrimSpace(repoArg)
|
||||||
|
if repoName == "" {
|
||||||
|
return "", "", "", errors.New("repository argument required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if at := strings.LastIndex(repoName, "@"); at >= 0 {
|
||||||
|
ref = strings.TrimSpace(repoName[at+1:])
|
||||||
|
repoName = strings.TrimSpace(repoName[:at])
|
||||||
|
if ref == "" || repoName == "" {
|
||||||
|
return "", "", "", errInvalidPkgInstallSource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(repoName, "/") {
|
||||||
|
parts := strings.Split(repoName, "/")
|
||||||
|
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||||
|
return "", "", "", errInvalidPkgInstallSource
|
||||||
|
}
|
||||||
|
org, repoName = parts[0], parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(repoName, "/") {
|
||||||
|
return "", "", "", errInvalidPkgInstallSource
|
||||||
|
}
|
||||||
|
|
||||||
|
return org, repoName, ref, nil
|
||||||
|
}
|
||||||
|
|
||||||
func addToRegistryFile(org, repoName string) error {
|
func addToRegistryFile(org, repoName string) error {
|
||||||
regPath, err := repos.FindRegistry(coreio.Local)
|
regPath, err := repos.FindRegistry(coreio.Local)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -146,6 +181,30 @@ func addToRegistryFile(org, repoName string) error {
|
||||||
return coreio.Local.Write(regPath, content)
|
return coreio.Local.Write(regPath, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clonePackageAtRef(ctx context.Context, org, repo, path, ref string) error {
|
||||||
|
if ghAuthenticated() {
|
||||||
|
httpsURL := fmt.Sprintf("https://github.com/%s/%s.git", org, repo)
|
||||||
|
args := []string{"repo", "clone", httpsURL, path, "--", "--branch", ref, "--single-branch"}
|
||||||
|
cmd := exec.CommandContext(ctx, "gh", args...)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
errStr := strings.TrimSpace(string(output))
|
||||||
|
if strings.Contains(errStr, "already exists") {
|
||||||
|
return errors.New(errStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"clone", "--branch", ref, "--single-branch", fmt.Sprintf("git@github.com:%s/%s.git", org, repo), path}
|
||||||
|
cmd := exec.CommandContext(ctx, "git", args...)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func detectRepoType(name string) string {
|
func detectRepoType(name string) string {
|
||||||
lower := strings.ToLower(name)
|
lower := strings.ToLower(name)
|
||||||
if strings.Contains(lower, "-mod-") || strings.HasSuffix(lower, "-mod") {
|
if strings.Contains(lower, "-mod-") || strings.HasSuffix(lower, "-mod") {
|
||||||
|
|
|
||||||
|
|
@ -67,3 +67,48 @@ func TestRunPkgInstall_InvalidRepoFormat_Bad(t *testing.T) {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "invalid repo format")
|
assert.Contains(t, err.Error(), "invalid repo format")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParsePkgInstallSource_Good(t *testing.T) {
|
||||||
|
t.Run("default org and repo", func(t *testing.T) {
|
||||||
|
org, repo, ref, err := parsePkgInstallSource("core-api")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "host-uk", org)
|
||||||
|
assert.Equal(t, "core-api", repo)
|
||||||
|
assert.Empty(t, ref)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("explicit org and ref", func(t *testing.T) {
|
||||||
|
org, repo, ref, err := parsePkgInstallSource("myorg/core-api@v1.2.3")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "myorg", org)
|
||||||
|
assert.Equal(t, "core-api", repo)
|
||||||
|
assert.Equal(t, "v1.2.3", ref)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunPkgInstall_WithRef_UsesRefClone_Good(t *testing.T) {
|
||||||
|
tmp := t.TempDir()
|
||||||
|
targetDir := filepath.Join(tmp, "packages")
|
||||||
|
|
||||||
|
originalGitCloneRef := gitCloneRef
|
||||||
|
t.Cleanup(func() {
|
||||||
|
gitCloneRef = originalGitCloneRef
|
||||||
|
})
|
||||||
|
|
||||||
|
var gotOrg, gotRepo, gotPath, gotRef string
|
||||||
|
gitCloneRef = func(_ context.Context, org, repoName, repoPath, ref string) error {
|
||||||
|
gotOrg = org
|
||||||
|
gotRepo = repoName
|
||||||
|
gotPath = repoPath
|
||||||
|
gotRef = ref
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := runPkgInstall("myorg/core-api@v1.2.3", targetDir, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "myorg", gotOrg)
|
||||||
|
assert.Equal(t, "core-api", gotRepo)
|
||||||
|
assert.Equal(t, filepath.Join(targetDir, "core-api"), gotPath)
|
||||||
|
assert.Equal(t, "v1.2.3", gotRef)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ var (
|
||||||
dimStyle = cli.DimStyle
|
dimStyle = cli.DimStyle
|
||||||
ghAuthenticated = cli.GhAuthenticated
|
ghAuthenticated = cli.GhAuthenticated
|
||||||
gitClone = cli.GitClone
|
gitClone = cli.GitClone
|
||||||
|
gitCloneRef = clonePackageAtRef
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddPkgCommands adds the 'pkg' command and subcommands for package management.
|
// AddPkgCommands adds the 'pkg' command and subcommands for package management.
|
||||||
|
|
|
||||||
|
|
@ -486,9 +486,19 @@ func ChooseMultiAction[T any](verb, subject string, items []T, opts ...ChooseOpt
|
||||||
// GitClone clones a GitHub repository to the specified path.
|
// GitClone clones a GitHub repository to the specified path.
|
||||||
// Prefers 'gh repo clone' if authenticated, falls back to SSH.
|
// Prefers 'gh repo clone' if authenticated, falls back to SSH.
|
||||||
func GitClone(ctx context.Context, org, repo, path string) error {
|
func GitClone(ctx context.Context, org, repo, path string) error {
|
||||||
|
return GitCloneRef(ctx, org, repo, path, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GitCloneRef clones a GitHub repository at a specific ref to the specified path.
|
||||||
|
// Prefers 'gh repo clone' if authenticated, falls back to SSH.
|
||||||
|
func GitCloneRef(ctx context.Context, org, repo, path, ref string) error {
|
||||||
if GhAuthenticated() {
|
if GhAuthenticated() {
|
||||||
httpsURL := fmt.Sprintf("https://github.com/%s/%s.git", org, repo)
|
httpsURL := fmt.Sprintf("https://github.com/%s/%s.git", org, repo)
|
||||||
cmd := exec.CommandContext(ctx, "gh", "repo", "clone", httpsURL, path)
|
args := []string{"repo", "clone", httpsURL, path}
|
||||||
|
if ref != "" {
|
||||||
|
args = append(args, "--", "--branch", ref, "--single-branch")
|
||||||
|
}
|
||||||
|
cmd := exec.CommandContext(ctx, "gh", args...)
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -499,7 +509,12 @@ func GitClone(ctx context.Context, org, repo, path string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fall back to SSH clone
|
// Fall back to SSH clone
|
||||||
cmd := exec.CommandContext(ctx, "git", "clone", fmt.Sprintf("git@github.com:%s/%s.git", org, repo), path)
|
args := []string{"clone"}
|
||||||
|
if ref != "" {
|
||||||
|
args = append(args, "--branch", ref, "--single-branch")
|
||||||
|
}
|
||||||
|
args = append(args, fmt.Sprintf("git@github.com:%s/%s.git", org, repo), path)
|
||||||
|
cmd := exec.CommandContext(ctx, "git", args...)
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New(strings.TrimSpace(string(output)))
|
return errors.New(strings.TrimSpace(string(output)))
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue