Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/agent/container/credentials_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ func configureGitUserLocally(
client tunnel.TunnelClient,
) error {
// get local credentials
localGitUser, err := gitcredentials.GetUser(userName)
localGitUser, err := gitcredentials.GetUser(userName, "")
if err != nil {
return err
} else if localGitUser.Name != "" && localGitUser.Email != "" {
Expand Down
6 changes: 5 additions & 1 deletion pkg/agent/tunnelserver/tunnelserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,11 @@ func (t *tunnelServer) DockerCredentials(
}

func (t *tunnelServer) GitUser(ctx context.Context, empty *tunnel.Empty) (*tunnel.Message, error) {
gitUser, err := gitcredentials.GetUser("")
workingDir := ""
if t.workspace != nil {
workingDir = t.workspace.Source.LocalFolder
}
gitUser, err := gitcredentials.GetUser("", workingDir)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/devcontainer/setup/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ func setupPlatformGitCredentials(
// setup platform git user
if platformOptions.UserCredentials.GitUser != "" &&
platformOptions.UserCredentials.GitEmail != "" {
gitUser, err := gitcredentials.GetUser(userName)
gitUser, err := gitcredentials.GetUser(userName, "")
if err == nil && gitUser.Name == "" && gitUser.Email == "" {
log.Info("Setup workspace git user and email")
err := gitcredentials.SetUser(userName, &gitcredentials.GitUser{
Expand Down
16 changes: 12 additions & 4 deletions pkg/gitcredentials/gitcredentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ func SetUser(userName string, user *GitUser) error {
return nil
}

func GetUser(userName string) (*GitUser, error) {
func GetUser(userName string, workingDir string) (*GitUser, error) {
gitUser := &GitUser{}

scopeArgs := []string{"config"}
Expand All @@ -195,15 +195,23 @@ func GetUser(userName string) (*GitUser, error) {
return gitUser, fmt.Errorf("get git global dir for %s: %w", userName, err)
}
scopeArgs = append(scopeArgs, "--file", p)
} else {
} else if workingDir == "" {
scopeArgs = append(scopeArgs, "--global")
}

nameCmd := exec.Command("git", append(scopeArgs, "user.name")...) // #nosec G204
emailCmd := exec.Command("git", append(scopeArgs, "user.email")...) // #nosec G204

if workingDir != "" {
nameCmd.Dir = workingDir
emailCmd.Dir = workingDir
}

// we ignore the error here, because if email is empty we don't care
name, _ := exec.Command("git", append(scopeArgs[:], "user.name")...).Output()
name, _ := nameCmd.Output()
gitUser.Name = strings.TrimSpace(string(name))

email, _ := exec.Command("git", append(scopeArgs[:], "user.email")...).Output()
email, _ := emailCmd.Output()
gitUser.Email = strings.TrimSpace(string(email))

return gitUser, nil
Expand Down
92 changes: 92 additions & 0 deletions pkg/gitcredentials/gitcredentials_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package gitcredentials

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetUser_WorkingDirResolvesIncludeIf(t *testing.T) {
tmpHome := t.TempDir()
t.Setenv("HOME", tmpHome)
t.Setenv("XDG_CONFIG_HOME", "")
t.Setenv("GIT_CONFIG_NOSYSTEM", "1")

projectDir := t.TempDir()
//nolint:gosec // test-only, args are constants
require.NoError(t, exec.Command("git", "init", projectDir).Run())

projectConfigPath := filepath.Join(tmpHome, "project.gitconfig")
require.NoError(t, os.WriteFile(projectConfigPath, []byte(`[user]
name = Project User
email = project@example.com
`), 0o600))

globalConfigPath := filepath.Join(tmpHome, ".gitconfig")
require.NoError(t, os.WriteFile(globalConfigPath, fmt.Appendf(nil, `[user]
name = Global User
email = global@example.com
[includeIf "gitdir:%s/"]
path = %s
`, projectDir, projectConfigPath), 0o600))

user, err := GetUser("", projectDir)
require.NoError(t, err)
assert.Equal(t, "Project User", user.Name)
assert.Equal(t, "project@example.com", user.Email)
}

func TestGetUser_EmptyWorkingDirUsesGlobal(t *testing.T) {
tmpHome := t.TempDir()
t.Setenv("HOME", tmpHome)
t.Setenv("XDG_CONFIG_HOME", "")
t.Setenv("GIT_CONFIG_NOSYSTEM", "1")

globalConfigPath := filepath.Join(tmpHome, ".gitconfig")
require.NoError(t, os.WriteFile(globalConfigPath, []byte(`[user]
name = Global User
email = global@example.com
`), 0o600))

user, err := GetUser("", "")
require.NoError(t, err)
assert.Equal(t, "Global User", user.Name)
assert.Equal(t, "global@example.com", user.Email)
}

func TestGetUser_WorkingDirWithNoMatchingIncludeIfUsesGlobal(t *testing.T) {
tmpHome := t.TempDir()
t.Setenv("HOME", tmpHome)
t.Setenv("XDG_CONFIG_HOME", "")
t.Setenv("GIT_CONFIG_NOSYSTEM", "1")

otherDir := t.TempDir()

projectConfigPath := filepath.Join(tmpHome, "project.gitconfig")
require.NoError(t, os.WriteFile(projectConfigPath, []byte(`[user]
name = Project User
email = project@example.com
`), 0o600))

globalConfigPath := filepath.Join(tmpHome, ".gitconfig")
require.NoError(t, os.WriteFile(globalConfigPath, fmt.Appendf(nil, `[user]
name = Global User
email = global@example.com
[includeIf "gitdir:%s/"]
path = %s
`, otherDir, projectConfigPath), 0o600))

projectDir := t.TempDir()
//nolint:gosec // test-only, args are constants
require.NoError(t, exec.Command("git", "init", projectDir).Run())

user, err := GetUser("", projectDir)
require.NoError(t, err)
assert.Equal(t, "Global User", user.Name)
assert.Equal(t, "global@example.com", user.Email)
}
13 changes: 7 additions & 6 deletions pkg/gitsshsigning/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,25 @@ const (
GPGFormatSSH = "ssh"
)

// ExtractGitConfiguration is used for extracting values from users local .gitconfig
// that are needed to setup devsy-ssh-signature helper inside the workspace.
func ExtractGitConfiguration() (string, string, error) {
format, err := readGitConfigValue(GPGFormatConfigKey)
func ExtractGitConfiguration(workingDir string) (string, string, error) {
format, err := readGitConfigValue(GPGFormatConfigKey, workingDir)
if err != nil {
return "", "", err
}

signingKey, err := readGitConfigValue(UsersSigningKeyConfigKey)
signingKey, err := readGitConfigValue(UsersSigningKeyConfigKey, workingDir)
if err != nil {
return "", "", err
}

return format, signingKey, nil
}

func readGitConfigValue(key string) (string, error) {
func readGitConfigValue(key string, workingDir string) (string, error) {
cmd := exec.Command("git", "config", "--get", key)
if workingDir != "" {
cmd.Dir = workingDir
}
output, err := cmd.Output()
if err != nil {
return "", err
Expand Down
63 changes: 63 additions & 0 deletions pkg/gitsshsigning/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package gitsshsigning

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestExtractGitConfiguration_WorkingDirResolvesIncludeIf(t *testing.T) {
tmpHome := t.TempDir()
t.Setenv("HOME", tmpHome)
t.Setenv("XDG_CONFIG_HOME", "")
t.Setenv("GIT_CONFIG_NOSYSTEM", "1")

projectDir := t.TempDir()
//nolint:gosec // test-only, args are constants
require.NoError(t, exec.Command("git", "init", projectDir).Run())

projectConfigPath := filepath.Join(tmpHome, "project.gitconfig")
require.NoError(t, os.WriteFile(projectConfigPath, []byte(`[gpg]
format = ssh
[user]
signingkey = /path/to/signing.pub
`), 0o600))

globalConfigPath := filepath.Join(tmpHome, ".gitconfig")
require.NoError(t, os.WriteFile(globalConfigPath, fmt.Appendf(nil, `[includeIf "gitdir:%s/"]
path = %s
`, projectDir, projectConfigPath), 0o600))

format, signingKey, err := ExtractGitConfiguration(projectDir)
require.NoError(t, err)
assert.Equal(t, GPGFormatSSH, format)
assert.Equal(t, "/path/to/signing.pub", signingKey)
}

func TestExtractGitConfiguration_WorkingDirWithGlobalSigningConfig(t *testing.T) {
tmpHome := t.TempDir()
t.Setenv("HOME", tmpHome)
t.Setenv("XDG_CONFIG_HOME", "")
t.Setenv("GIT_CONFIG_NOSYSTEM", "1")

projectDir := t.TempDir()
//nolint:gosec // test-only, args are constants
require.NoError(t, exec.Command("git", "init", projectDir).Run())

globalConfigPath := filepath.Join(tmpHome, ".gitconfig")
require.NoError(t, os.WriteFile(globalConfigPath, []byte(`[gpg]
format = ssh
[user]
signingkey = /global/key.pub
`), 0o600))

format, signingKey, err := ExtractGitConfiguration(projectDir)
require.NoError(t, err)
assert.Equal(t, GPGFormatSSH, format)
assert.Equal(t, "/global/key.pub", signingKey)
}
10 changes: 7 additions & 3 deletions pkg/tunnel/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,10 @@ func runTunnelServer(ctx context.Context, cancel context.CancelFunc, p tunnelSer
// When explicitKey is set (from --git-ssh-signing-key flag), it takes
// precedence over the host's .gitconfig. This ensures signing works
// even when user.signingkey is not in the host git configuration.
func addGitSSHSigningKey(command string, explicitKey string) string {
func addGitSSHSigningKey(command string, explicitKey string, workingDir string) string {
userSigningKey := explicitKey
if userSigningKey == "" {
format, extracted, err := gitsshsigning.ExtractGitConfiguration()
format, extracted, err := gitsshsigning.ExtractGitConfiguration(workingDir)
if err != nil {
log.Debugf("failed to extract git configuration: %v", err)
return command
Expand All @@ -138,7 +138,11 @@ func buildCredentialsCommand(opts RunServicesOptions) string {
command += " --configure-git-helper"
}
if opts.ConfigureGitSSHSignatureHelper {
command = addGitSSHSigningKey(command, opts.GitSSHSigningKey)
workingDir := ""
if opts.Workspace != nil {
workingDir = opts.Workspace.Source.LocalFolder
}
command = addGitSSHSigningKey(command, opts.GitSSHSigningKey, workingDir)
}
if opts.ConfigureDockerCredentials {
command += " --configure-docker-helper"
Expand Down
6 changes: 3 additions & 3 deletions pkg/tunnel/services_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const testBaseCommand = "devsy agent container credentials-server --user root"

func TestAddGitSSHSigningKey_ExplicitKey(t *testing.T) {
command := testBaseCommand
result := addGitSSHSigningKey(command, "/path/to/key.pub")
result := addGitSSHSigningKey(command, "/path/to/key.pub", "")

encoded := base64.StdEncoding.EncodeToString([]byte("/path/to/key.pub"))
assert.Equal(t, command+" --git-user-signing-key "+encoded, result)
Expand All @@ -22,7 +22,7 @@ func TestAddGitSSHSigningKey_ExplicitKeyTakesPrecedence(t *testing.T) {
// of what ExtractGitConfiguration might return from host .gitconfig.
command := testBaseCommand
explicitKey := "/explicit/key.pub"
result := addGitSSHSigningKey(command, explicitKey)
result := addGitSSHSigningKey(command, explicitKey, "")

encoded := base64.StdEncoding.EncodeToString([]byte(explicitKey))
assert.Equal(t, command+" --git-user-signing-key "+encoded, result)
Expand All @@ -35,7 +35,7 @@ func TestAddGitSSHSigningKey_EmptyExplicitKey_FallsBackToHostConfig(t *testing.T
t.Setenv("HOME", tmpHome)
t.Setenv("XDG_CONFIG_HOME", tmpHome)

result := addGitSSHSigningKey(command, "")
result := addGitSSHSigningKey(command, "", "")

assert.Equal(t, command, result)
assert.NotContains(t, result, "--git-user-signing-key")
Expand Down
Loading