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 go/cmd/dolt/commands/remote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func TestRemoteAdd_StoresNormalizedGitUrl(t *testing.T) {
normalized, ok, err := env.NormalizeGitRemoteUrl(original)
assert.NoError(t, err)
assert.True(t, ok)
assert.Equal(t, "git+ssh://git@github.com/timsehn/dolt-in-git.git", normalized)
assert.Equal(t, "git+ssh://git@github.com/./timsehn/dolt-in-git.git", normalized)

urlToStore := original
if strings.HasPrefix(strings.ToLower(scheme), "git+") {
Expand Down
22 changes: 20 additions & 2 deletions go/libraries/doltcore/dbfactory/git_remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,11 @@ func (fact GitRemoteFactory) CreateDB(ctx context.Context, nbf *types.NomsBinFor
}

// Ensure the configured git remote exists and points to the underlying git remote URL.
if err := ensureGitRemoteURL(ctx, cacheRepo, remoteName, remoteURL.String()); err != nil {
gitURL := gitRemoteURLString(remoteURL)
if err := ensureGitRemoteURL(ctx, cacheRepo, remoteName, gitURL); err != nil {
return nil, nil, nil, err
}
if err := ensureRemoteHasBranches(ctx, cacheRepo, remoteName, remoteURL.String()); err != nil {
if err := ensureRemoteHasBranches(ctx, cacheRepo, remoteName, gitURL); err != nil {
return nil, nil, nil, err
}

Expand Down Expand Up @@ -186,6 +187,23 @@ func parseGitRemoteFactoryURL(urlObj *url.URL, params map[string]interface{}) (r
return &cp, ref, nil
}

// gitRemoteURLString converts a parsed remote URL back to a string suitable for
// passing to git commands. If the URL has an ssh scheme and the path starts with
// "/./", it was originally an SCP-style relative path (e.g. git@host:path.git).
// We reconstruct SCP-style format so git treats the path as relative to the SSH
// user's home directory.
func gitRemoteURLString(u *url.URL) string {
if strings.ToLower(u.Scheme) == "ssh" && strings.HasPrefix(u.Path, "/./") {
// Reconstruct SCP-style: [user@]host:relativePath
host := u.Host
if u.User != nil {
host = u.User.String() + "@" + u.Host
}
return host + ":" + strings.TrimPrefix(u.Path, "/./")
}
return u.String()
}

func resolveGitRemoteRef(params map[string]interface{}) string {
// Prefer an explicit remote parameter (e.g. from `--ref`).
if params != nil {
Expand Down
38 changes: 38 additions & 0 deletions go/libraries/doltcore/dbfactory/git_remote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"crypto/sha256"
"encoding/hex"
"net/url"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -49,6 +50,43 @@ func shortTempDir(t *testing.T) string {
return dir
}

func TestGitRemoteURLString(t *testing.T) {
tests := []struct {
name string
rawURL string
expected string
}{
{
name: "ssh relative path reconstructs SCP-style",
rawURL: "ssh://git@myhost/./relative/repo.git",
expected: "git@myhost:relative/repo.git",
},
{
name: "ssh absolute path unchanged",
rawURL: "ssh://git@myhost/abs/repo.git",
expected: "ssh://git@myhost/abs/repo.git",
},
{
name: "https unchanged",
rawURL: "https://example.com/org/repo.git",
expected: "https://example.com/org/repo.git",
},
{
name: "ssh no user relative path",
rawURL: "ssh://myhost/./relative/repo.git",
expected: "myhost:relative/repo.git",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u, err := url.Parse(tt.rawURL)
require.NoError(t, err)
got := gitRemoteURLString(u)
require.Equal(t, tt.expected, got)
})
}
}

func TestGitRemoteFactory_GitFile_RequiresGitCacheRootParam(t *testing.T) {
ctx := context.Background()
_, _, _, err := CreateDB(ctx, types.Format_Default, "git+file:///tmp/remote.git", map[string]interface{}{})
Expand Down
10 changes: 9 additions & 1 deletion go/libraries/doltcore/env/git_remote_url.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,15 @@ func NormalizeGitRemoteUrl(urlArg string) (normalized string, ok bool, err error
// scp-like ssh: [user@]host:path/repo.git (no scheme, no ://)
if isScpLikeGitRemote(urlArg) {
host, p := splitScpLike(urlArg)
ssh := "git+ssh://" + host + "/" + strings.TrimPrefix(p, "/")
var pathPart string
if strings.HasPrefix(p, "/") {
// Absolute path: git@host:/abs/repo.git → /abs/repo.git
pathPart = p
} else {
// Relative path: git@host:path.git → /./path.git
pathPart = "/./" + p
}
ssh := "git+ssh://" + host + pathPart
u, err := url.Parse(ssh)
if err != nil {
return "", false, err
Expand Down
11 changes: 9 additions & 2 deletions go/libraries/doltcore/env/git_remote_url_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,18 @@ func TestNormalizeGitRemoteUrl(t *testing.T) {
require.Equal(t, "git+https://example.com/org/repo.git", got)
})

t.Run("scp-style becomes git+ssh", func(t *testing.T) {
t.Run("scp-style relative becomes git+ssh with dot marker", func(t *testing.T) {
got, ok, err := NormalizeGitRemoteUrl("git@github.com:org/repo.git")
require.NoError(t, err)
require.True(t, ok)
require.Equal(t, "git+ssh://git@github.com/org/repo.git", got)
require.Equal(t, "git+ssh://git@github.com/./org/repo.git", got)
})

t.Run("scp-style absolute becomes git+ssh without dot marker", func(t *testing.T) {
got, ok, err := NormalizeGitRemoteUrl("git@host:/abs/repo.git")
require.NoError(t, err)
require.True(t, ok)
require.Equal(t, "git+ssh://git@host/abs/repo.git", got)
})

t.Run("schemeless host/path defaults to git+https", func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion integration-tests/bats/remote-cmd.bats
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ teardown() {

run dolt remote -v
[ "$status" -eq 0 ]
[[ "$output" =~ origin[[:blank:]]git[+]ssh://git@github.com/org/repo[.]git ]] || false
[[ "$output" =~ origin[[:blank:]]git[+]ssh://git@github.com/[.]/org/repo[.]git ]] || false
}

@test "remote-cmd: stores normalized git+https url for https .git input" {
Expand Down
119 changes: 119 additions & 0 deletions integration-tests/bats/remotes-git.bats
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,125 @@ seed_git_remote_branch() {
[[ "$output" =~ "terminal prompts disabled" ]] || false
}

@test "remotes-git: scp-style relative path is not converted to absolute (#10564)" {
# Regression test for https://github.com/dolthub/dolt/issues/10564
# When a user adds a remote as `git@host:relative/repo.git` (SCP-style,
# relative path), the path must NOT become absolute when passed to git.
# Bug: dolt was converting this to `ssh://git@host/relative/repo.git`
# which git interprets as absolute path `/relative/repo.git`.

install_fake_git_url_recorder

old_path="$PATH"
export PATH="$BATS_TMPDIR/fakebin:$PATH"
export DOLT_IGNORE_LOCK_FILE=1

mkdir repo1
cd repo1
dolt init
dolt commit --allow-empty -m "init"
dolt remote add origin git@myhost:relative/repo.git

# Trigger CreateDB → ensureGitRemoteURL → git remote add (in internal cache repo).
# The push will ultimately fail (fake host), but we only need the remote-add to fire.
run dolt push origin main

export PATH="$old_path"

# The fake git recorded the URL it received for `git remote add`.
[ -f "$BATS_TMPDIR/recorded_remote_urls" ]
recorded_url=$(tail -1 "$BATS_TMPDIR/recorded_remote_urls")

# The URL passed to git must NOT convert the relative SCP path into an
# absolute SSH path. `ssh://git@myhost/relative/repo.git` is wrong because
# the leading `/` makes git send `git-upload-pack '/relative/repo.git'` to
# the server — an absolute filesystem path.
#
# Acceptable forms:
# git@myhost:relative/repo.git (SCP-style, preserves relative)
# ssh://git@myhost/./relative/repo.git (explicit relative marker)
if [[ "$recorded_url" != "git@myhost:relative/repo.git" && \
"$recorded_url" != "ssh://git@myhost/./relative/repo.git" ]]; then
echo "BUG: Unexpected URL passed to git remote add: $recorded_url"
echo "Expected one of:"
echo " git@myhost:relative/repo.git"
echo " ssh://git@myhost/./relative/repo.git"
echo "Got: $recorded_url"
false
fi
}

install_fake_git_url_recorder() {
# Fake git that records the URL passed to `git remote add` so tests can
# inspect whether SCP-style relative paths survive normalization intact.
# Behaves like install_fake_git_auth_failure but also writes the remote-add
# URL to a well-known file for later assertion.
mkdir -p "$BATS_TMPDIR/fakebin"
cat > "$BATS_TMPDIR/fakebin/git" <<'FAKEGIT'
#!/usr/bin/env bash
set -euo pipefail

git_dir=""
if [[ "${1:-}" == "--git-dir" ]]; then
git_dir="${2:-}"
shift 2
fi

cmd="${1:-}"
shift || true

case "$cmd" in
init)
if [[ "${1:-}" == "--bare" ]]; then
mkdir -p "$git_dir"
exit 0
fi
;;
remote)
sub="${1:-}"; shift || true
case "$sub" in
get-url)
shift || true # consume --
name="${1:-}"
f="${git_dir}/remote_${name}_url"
if [[ -f "$f" ]]; then
cat "$f"
exit 0
fi
echo "fatal: No such remote '$name'" >&2
exit 2
;;
add|set-url)
shift || true # consume --
name="${1:-}"; url="${2:-}"
mkdir -p "$git_dir"
printf "%s" "$url" > "${git_dir}/remote_${name}_url"
# Record URL for test assertions.
printf "%s\n" "$url" >> "${BATS_TMPDIR}/recorded_remote_urls"
exit 0
;;
esac
;;
ls-remote)
# Return a dummy branch so ensureRemoteHasBranches succeeds, but the
# subsequent fetch will fail. That's fine — we only need the remote-add
# URL to be recorded.
echo "0000000000000000000000000000000000000000 refs/heads/main"
exit 0
;;
fetch)
echo "fatal: could not connect to 'myhost'" >&2
exit 128
;;
esac

echo "fatal: unknown command" >&2
exit 128
FAKEGIT
chmod +x "$BATS_TMPDIR/fakebin/git"
rm -f "$BATS_TMPDIR/recorded_remote_urls"
}

install_fake_git_auth_failure() {
mkdir -p "$BATS_TMPDIR/fakebin"
cat > "$BATS_TMPDIR/fakebin/git" <<'EOF'
Expand Down
Loading