diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 7ad50f3..be2da5c 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -7,7 +7,7 @@ on: pull_request: env: - GO_VERSION: 1.22.6 + GO_VERSION: 1.23.2 jobs: setup: diff --git a/Makefile b/Makefile index c2c41d1..ffc1479 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -ROOT_PACKAGE := github.com/n-r-w/protodep +ROOT_PACKAGE := github.com/torqio/protodep VERSION_PACKAGE := $(ROOT_PACKAGE)/version LDFLAG_GIT_COMMIT := "$(VERSION_PACKAGE).gitCommit" LDFLAG_GIT_COMMIT_FULL := "$(VERSION_PACKAGE).gitCommitFull" diff --git a/README.md b/README.md index bb4466a..0d66719 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ protodep - dependency tool for Protocol Buffers IDL file (.proto) vendoring tool ======= -## Evolution of , which unfortunately has not been updated by the author for a long time +## Evolution of , which is the evolution of -## What's new in this fork (compared to the original project) +## What's new in this fork (compared to n-r-w fork) +- [x] Added support for git credential helpers +### Stuff added in n-r-w's fork - [x] Added support for .netrc file - [x] Added support for local proto files import - [x] Added support for gitlab subgroups @@ -22,7 +24,7 @@ If you manage proto files in a git repository, what will you do? Most remote ser ### go install ```bash -go install -v github.com/n-r-w/protodep@latest +go install -v github.com/torqio/protodep@latest ``` ## Usage diff --git a/cmd/up.go b/cmd/up.go index 9c76495..1055c31 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -7,8 +7,8 @@ import ( "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" - "github.com/n-r-w/protodep/internal/logger" - "github.com/n-r-w/protodep/internal/resolver" + "github.com/torqio/protodep/internal/logger" + "github.com/torqio/protodep/internal/resolver" ) var upCmd = &cobra.Command{ @@ -47,6 +47,12 @@ var upCmd = &cobra.Command{ } logger.Info("use netrc = %t", useNetrc) + useGitCredentials, err := cmd.Flags().GetBool("use-git-credentials") + if err != nil { + return err + } + logger.Info("use git credentials = %t", useGitCredentials) + basicAuthUsername, err := cmd.Flags().GetString("basic-auth-username") if err != nil { return err @@ -74,15 +80,16 @@ var upCmd = &cobra.Command{ } conf := resolver.Config{ - UseHttps: useHTTPS, - UseNetrc: useNetrc, - HomeDir: homeDir, - TargetDir: pwd, - OutputDir: pwd, - BasicAuthUsername: basicAuthUsername, - BasicAuthPassword: basicAuthPassword, - IdentityFile: identityFile, - IdentityPassword: password, + UseHttps: useHTTPS, + UseGitCredentialsHelper: useGitCredentials, + UseNetrc: useNetrc, + HomeDir: homeDir, + TargetDir: pwd, + OutputDir: pwd, + BasicAuthUsername: basicAuthUsername, + BasicAuthPassword: basicAuthPassword, + IdentityFile: identityFile, + IdentityPassword: password, } httpsProvider, err := conf.GetHttpsAuthProvider() @@ -108,8 +115,10 @@ func initDepCmd() { upCmd.PersistentFlags().StringP("identity-file", "i", "", "set the identity file for SSH") upCmd.PersistentFlags().StringP("password", "p", "", "set the password for SSH") upCmd.PersistentFlags().BoolP("cleanup", "c", false, "cleanup cache before exec.") - upCmd.PersistentFlags().BoolP("use-https", "u", false, "use HTTPS to get dependencies.") - upCmd.PersistentFlags().BoolP("use-netrc", "n", true, "use netrc file for authentication") + upCmd.PersistentFlags().BoolP("use-https", "u", true, "use HTTPS to get dependencies.") + upCmd.PersistentFlags().BoolP("use-netrc", "n", false, "use netrc file for authentication") + upCmd.PersistentFlags().BoolP("use-git-credentials", "m", true, "use git credentials for authentication") + upCmd.PersistentFlags().StringP("basic-auth-username", "", "", "set the username with Basic Auth via HTTPS") upCmd.PersistentFlags().StringP("basic-auth-password", "", "", "set the password or personal access token(when enabled 2FA) with Basic Auth via HTTPS") } diff --git a/cmd/version.go b/cmd/version.go index b903973..b39b79c 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -5,7 +5,7 @@ import ( "github.com/spf13/cobra" - "github.com/n-r-w/protodep/version" + "github.com/torqio/protodep/version" ) const art = ` diff --git a/go.mod b/go.mod index d030154..41b6904 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/n-r-w/protodep +module github.com/torqio/protodep go 1.23 diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 2bb642b..b0b23e2 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -8,21 +8,21 @@ import ( func TestGetRepositoryURLWithSSH(t *testing.T) { target := &AuthProviderWithSSH{} - actual := target.GetRepositoryURL("github.com/n-r-w/protodep") + actual := target.GetRepositoryURL("github.com/torqio/protodep") - require.Equal(t, "ssh://github.com/n-r-w/protodep.git", actual) + require.Equal(t, "ssh://github.com/torqio/protodep.git", actual) } func TestGetRepositoryURLWithSSHAgent(t *testing.T) { target := &AuthProviderWithSSHAgent{} - actual := target.GetRepositoryURL("github.com/n-r-w/protodep") + actual := target.GetRepositoryURL("github.com/torqio/protodep") - require.Equal(t, "ssh://github.com/n-r-w/protodep.git", actual) + require.Equal(t, "ssh://github.com/torqio/protodep.git", actual) } func TestGetRepositoryURLHTTPS(t *testing.T) { target := &AuthProviderHTTPS{} - actual := target.GetRepositoryURL("github.com/n-r-w/protodep") + actual := target.GetRepositoryURL("github.com/torqio/protodep") - require.Equal(t, "https://github.com/n-r-w/protodep.git", actual) + require.Equal(t, "https://github.com/torqio/protodep.git", actual) } diff --git a/internal/repository/git.go b/internal/repository/git.go index 2e90062..fb6f775 100644 --- a/internal/repository/git.go +++ b/internal/repository/git.go @@ -8,9 +8,9 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" - "github.com/n-r-w/protodep/internal/auth" - "github.com/n-r-w/protodep/internal/config" - "github.com/n-r-w/protodep/internal/logger" + "github.com/torqio/protodep/internal/auth" + "github.com/torqio/protodep/internal/config" + "github.com/torqio/protodep/internal/logger" ) const masterBranch = "master" diff --git a/internal/resolver/config.go b/internal/resolver/config.go index 584f722..4e733e0 100644 --- a/internal/resolver/config.go +++ b/internal/resolver/config.go @@ -4,14 +4,17 @@ import ( "fmt" "path/filepath" - "github.com/n-r-w/protodep/internal/auth" - "github.com/n-r-w/protodep/internal/logger" + "github.com/torqio/protodep/internal/auth" + "github.com/torqio/protodep/internal/logger" ) type Config struct { // UseHttps will force https on each proto dependencies fetch. UseHttps bool + // UseGitCredentialsHelper will use git credentials helper for authentication. + UseGitCredentialsHelper bool + // UseNetrc will use netrc file for authentication. UseNetrc bool diff --git a/internal/resolver/credentials.go b/internal/resolver/credentials.go new file mode 100644 index 0000000..2e0f668 --- /dev/null +++ b/internal/resolver/credentials.go @@ -0,0 +1,196 @@ +package resolver + +import ( + "bytes" + "fmt" + "github.com/go-git/go-git/v5/config" + format "github.com/go-git/go-git/v5/plumbing/format/config" + "net/url" + "os/exec" + "strings" +) + +const credentialSection = "credential" + +type CredentialConfigEntry struct { + Helper []string + Username string + UseHttpPath bool +} + +type Credentials map[string]*CredentialConfigEntry + +type GitCredential struct { + Protocol string + Host string + Username string + Password string + URL string +} + +func (c GitCredential) String() string { + var b strings.Builder + + if c.Protocol != "" { + fmt.Fprintf(&b, "protocol=%s\n", c.Protocol) + } + + // If full URL is provided, use that + if c.URL != "" { + fmt.Fprintf(&b, "url=%s\n", c.URL) + } + + // Otherwise fall back to protocol/host + if c.Host != "" { + fmt.Fprintf(&b, "host=%s\n", c.Host) + } + + if c.Username != "" { + fmt.Fprintf(&b, "username=%s\n", c.Username) + } + if c.Password != "" { + fmt.Fprintf(&b, "password=%s\n", c.Password) + } + return b.String() +} + +func newCredential(opts format.Options) *CredentialConfigEntry { + return &CredentialConfigEntry{ + Helper: opts.GetAll("helper"), + Username: opts.Get("username"), + UseHttpPath: opts.Get("usehttppath") == "true", + } +} + +func buildCredentialCommand(helperName, action string) (*exec.Cmd, error) { + if strings.HasPrefix(helperName, "!") { + // For helpers starting with !, execute the command directly + cmdStr := strings.TrimPrefix(helperName, "!") + cmdParts := strings.Fields(cmdStr) + if len(cmdParts) == 0 { + return nil, fmt.Errorf("invalid credential helper command: %s", helperName) + } + + // Append the action to the command parts + cmdParts = append(cmdParts, action) + return exec.Command(cmdParts[0], cmdParts[1:]...), nil + } + + // Regular git credential helper + return exec.Command("git", "credential-"+helperName, action), nil +} + +func invokeCredentialHelper(helperName, action string, cred GitCredential) (GitCredential, error) { + cmd, err := buildCredentialCommand(helperName, action) + if err != nil { + return GitCredential{}, err + } + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + cmd.Stdin = strings.NewReader(cred.String()) + + if err := cmd.Run(); err != nil { + return GitCredential{}, fmt.Errorf("credential helper failed: %v, stderr: %s", err, stderr.String()) + } + + // Parse output + output := stdout.String() + result := GitCredential{} + + for _, line := range strings.Split(output, "\n") { + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + switch parts[0] { + case "protocol": + result.Protocol = parts[1] + case "host": + result.Host = parts[1] + case "username": + result.Username = parts[1] + case "password": + result.Password = parts[1] + } + } + + return result, nil +} + +var ErrNoCredentialHelperFound = fmt.Errorf("no credential helper found") + +func (c *CredentialConfigEntry) Evaluate(repoURL string) (*GitCredential, error) { + parsedUrl, err := url.Parse(repoURL) + if err != nil { + return nil, err + } + + testedUrl := repoURL + if !c.UseHttpPath { + testedUrl = parsedUrl.Scheme + "://" + parsedUrl.Host + } + + cred := GitCredential{ + Protocol: parsedUrl.Scheme, + Host: parsedUrl.Host, + URL: testedUrl, + Username: c.Username, + } + + for _, helperName := range c.Helper { + cred, err = invokeCredentialHelper(helperName, "get", cred) + if err != nil { + return nil, err + } + + return &cred, nil + } + + return nil, ErrNoCredentialHelperFound +} + +func ParseGitCredentials() (Credentials, error) { + c, err := config.LoadConfig(config.GlobalScope) + if err != nil { + return nil, err + } + + sect := c.Raw.Section(credentialSection) + + result := make(map[string]*CredentialConfigEntry) + result["default"] = newCredential(sect.Options) + + for _, sub := range sect.Subsections { + result[sub.Name] = newCredential(sub.Options) + } + + return result, nil +} + +func (c Credentials) Has(section string) bool { + _, ok := c[section] + return ok +} + +func (c Credentials) Get(host string) *CredentialConfigEntry { + + parsedURL, err := url.Parse(host) + if err != nil { + return c["default"] + } + + if parsedURL.Scheme == "" { + parsedURL.Scheme = "https" + } + + checkTargets := []string{parsedURL.Scheme + "://" + parsedURL.Host, "default"} + for _, target := range checkTargets { + if c.Has(target) { + return c[target] + } + } + + return nil +} diff --git a/internal/resolver/credentials_test.go b/internal/resolver/credentials_test.go new file mode 100644 index 0000000..2d43ea0 --- /dev/null +++ b/internal/resolver/credentials_test.go @@ -0,0 +1,29 @@ +package resolver + +import ( + "github.com/stretchr/testify/require" + "path" + "path/filepath" + "runtime" + "testing" +) + +func currentDir() string { + _, filename, _, _ := runtime.Caller(1) + return filepath.Dir(filename) +} + +func TestParseGitCredentials(t *testing.T) { + t.Setenv("HOME", path.Join(currentDir(), "testdata")) + + creds, err := ParseGitCredentials() + require.NoError(t, err) + require.Len(t, creds, 3) + + def := creds.Get("https://invalid.com") + require.Equal(t, def, creds["default"]) + + githubCred := creds.Get("https://github.com/repo/name") + require.Equal(t, creds["https://github.com"], githubCred) + require.Equal(t, githubCred.Helper, []string{"!/opt/homebrew/bin/gh auth git-credential"}) +} diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go index d7191e0..383c6d9 100644 --- a/internal/resolver/resolver.go +++ b/internal/resolver/resolver.go @@ -1,16 +1,17 @@ package resolver import ( + "errors" "fmt" "os" "path/filepath" "strings" "github.com/gobwas/glob" - "github.com/n-r-w/protodep/internal/auth" - "github.com/n-r-w/protodep/internal/config" - "github.com/n-r-w/protodep/internal/logger" - "github.com/n-r-w/protodep/internal/repository" + "github.com/torqio/protodep/internal/auth" + "github.com/torqio/protodep/internal/config" + "github.com/torqio/protodep/internal/logger" + "github.com/torqio/protodep/internal/repository" ) type protoResource struct { @@ -21,8 +22,9 @@ type protoResource struct { type Resolver struct { conf *Config - httpsProvider auth.AuthProvider - sshProvider auth.AuthProvider + httpsProvider auth.AuthProvider + sshProvider auth.AuthProvider + gitCredentialsProvider Credentials netrcInfo []netrcLine } @@ -41,6 +43,14 @@ func New(conf *Config, httpsProvider, sshProvider auth.AuthProvider) (*Resolver, s.netrcInfo = netrcInfo + // try to parse git credentials + gitCredentialsProvider, err := ParseGitCredentials() + if err != nil { + logger.Error("failed to parse git credentials: %v", err) + } + + s.gitCredentialsProvider = gitCredentialsProvider + return s, nil } @@ -151,16 +161,41 @@ func (s *Resolver) getRepository(dep config.ProtoDepDependency, protodepDir stri if userPassword == "" { return nil, fmt.Errorf("auth_password_env %s is empty", dep.PasswordEnv) } + } else if s.conf.UseGitCredentialsHelper && s.gitCredentialsProvider != nil { + targetRepo := dep.Repository() + if s.conf.UseHttps { + targetRepo = "https://" + targetRepo + } + + cred := s.gitCredentialsProvider.Get(targetRepo) + if cred != nil { + evalutedCreds, err := cred.Evaluate(targetRepo) + if err != nil { + if !errors.Is(err, ErrNoCredentialHelperFound) { + logger.Warn("failed to evaluate git credentials for %s: %v", targetRepo, err) + } + } else { + logger.Info("using git credentials for %s", targetRepo) + userName = evalutedCreds.Username + userPassword = evalutedCreds.Password + } + } } else if s.conf.UseNetrc { machine := dep.Machine() + machineFound := false for _, netrc := range s.netrcInfo { if netrc.machine == machine && netrc.login != "" && netrc.password != "" { userName = netrc.login userPassword = netrc.password + machineFound = true break } } + + if !machineFound { + return nil, fmt.Errorf("machine %s not found in netrc", machine) + } } if s.conf.UseHttps || dep.Protocol == "https" || (dep.Protocol == "" && userName != "") { diff --git a/internal/resolver/resolver_test.go b/internal/resolver/resolver_test.go index d5c254f..5ef295e 100644 --- a/internal/resolver/resolver_test.go +++ b/internal/resolver/resolver_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" - "github.com/n-r-w/protodep/internal/auth" + "github.com/torqio/protodep/internal/auth" ) func TestSync(t *testing.T) { diff --git a/internal/resolver/testdata/.gitconfig b/internal/resolver/testdata/.gitconfig new file mode 100644 index 0000000..8c60dd3 --- /dev/null +++ b/internal/resolver/testdata/.gitconfig @@ -0,0 +1,6 @@ +[credential "https://dev.azure.com"] + useHttpPath = true +[credential "https://github.com"] + helper = !/opt/homebrew/bin/gh auth git-credential +[credential] + helper = !/opt/homebrew/bin/gh auth git-credential diff --git a/main.go b/main.go index c677e53..63541c5 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,7 @@ package main import ( - "github.com/n-r-w/protodep/cmd" + "github.com/torqio/protodep/cmd" ) func main() {