Skip to content

Commit db43f6a

Browse files
authored
Merge release 0.19.30rc hotfix (#3121)
2 parents 2e9d8c1 + fda5199 commit db43f6a

File tree

2 files changed

+123
-12
lines changed

2 files changed

+123
-12
lines changed

src/dstack/_internal/core/services/repos.py

Lines changed: 101 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,32 +36,76 @@ def get_repo_creds_and_default_branch(
3636

3737
# no auth
3838
with suppress(InvalidRepoCredentialsError):
39-
return _get_repo_creds_and_default_branch_https(url)
39+
creds, default_branch = _get_repo_creds_and_default_branch_https(url)
40+
logger.debug(
41+
"Git repo %s is public. Using no auth. Default branch: %s", repo_url, default_branch
42+
)
43+
return creds, default_branch
4044

4145
# ssh key provided by the user or pulled from the server
4246
if identity_file is not None or private_key is not None:
4347
if identity_file is not None:
4448
private_key = _read_private_key(identity_file)
45-
return _get_repo_creds_and_default_branch_ssh(url, identity_file, private_key)
49+
creds, default_branch = _get_repo_creds_and_default_branch_ssh(
50+
url, identity_file, private_key
51+
)
52+
logger.debug(
53+
"Git repo %s is private. Using identity file: %s. Default branch: %s",
54+
repo_url,
55+
identity_file,
56+
default_branch,
57+
)
58+
return creds, default_branch
4659
elif private_key is not None:
4760
with NamedTemporaryFile("w+", 0o600) as f:
4861
f.write(private_key)
4962
f.flush()
50-
return _get_repo_creds_and_default_branch_ssh(url, f.name, private_key)
63+
creds, default_branch = _get_repo_creds_and_default_branch_ssh(
64+
url, f.name, private_key
65+
)
66+
masked_key = "***" + private_key[-10:] if len(private_key) > 10 else "***MASKED***"
67+
logger.debug(
68+
"Git repo %s is private. Using private key: %s. Default branch: %s",
69+
repo_url,
70+
masked_key,
71+
default_branch,
72+
)
73+
return creds, default_branch
5174
else:
5275
assert False, "should not reach here"
5376

5477
# oauth token provided by the user or pulled from the server
5578
if oauth_token is not None:
56-
return _get_repo_creds_and_default_branch_https(url, oauth_token)
79+
creds, default_branch = _get_repo_creds_and_default_branch_https(url, oauth_token)
80+
masked_token = (
81+
len(oauth_token[:-4]) * "*" + oauth_token[-4:]
82+
if len(oauth_token) > 4
83+
else "***MASKED***"
84+
)
85+
logger.debug(
86+
"Git repo %s is private. Using provided OAuth token: %s. Default branch: %s",
87+
repo_url,
88+
masked_token,
89+
default_branch,
90+
)
91+
return creds, default_branch
5792

5893
# key from ssh config
5994
identities = get_host_config(url.original_host).get("identityfile")
6095
if identities:
6196
_identity_file = identities[0]
6297
with suppress(InvalidRepoCredentialsError):
6398
_private_key = _read_private_key(_identity_file)
64-
return _get_repo_creds_and_default_branch_ssh(url, _identity_file, _private_key)
99+
creds, default_branch = _get_repo_creds_and_default_branch_ssh(
100+
url, _identity_file, _private_key
101+
)
102+
logger.debug(
103+
"Git repo %s is private. Using SSH config identity file: %s. Default branch: %s",
104+
repo_url,
105+
_identity_file,
106+
default_branch,
107+
)
108+
return creds, default_branch
65109

66110
# token from gh config
67111
if os.path.exists(gh_config_path):
@@ -70,13 +114,35 @@ def get_repo_creds_and_default_branch(
70114
_oauth_token = gh_hosts.get(url.host, {}).get("oauth_token")
71115
if _oauth_token is not None:
72116
with suppress(InvalidRepoCredentialsError):
73-
return _get_repo_creds_and_default_branch_https(url, _oauth_token)
117+
creds, default_branch = _get_repo_creds_and_default_branch_https(url, _oauth_token)
118+
masked_token = (
119+
len(_oauth_token[:-4]) * "*" + _oauth_token[-4:]
120+
if len(_oauth_token) > 4
121+
else "***MASKED***"
122+
)
123+
logger.debug(
124+
"Git repo %s is private. Using GitHub config token: %s from %s. Default branch: %s",
125+
repo_url,
126+
masked_token,
127+
gh_config_path,
128+
default_branch,
129+
)
130+
return creds, default_branch
74131

75132
# default user key
76133
if os.path.exists(default_ssh_key):
77134
with suppress(InvalidRepoCredentialsError):
78135
_private_key = _read_private_key(default_ssh_key)
79-
return _get_repo_creds_and_default_branch_ssh(url, default_ssh_key, _private_key)
136+
creds, default_branch = _get_repo_creds_and_default_branch_ssh(
137+
url, default_ssh_key, _private_key
138+
)
139+
logger.debug(
140+
"Git repo %s is private. Using default identity file: %s. Default branch: %s",
141+
repo_url,
142+
default_ssh_key,
143+
default_branch,
144+
)
145+
return creds, default_branch
80146

81147
raise InvalidRepoCredentialsError(
82148
"No valid default Git credentials found. Pass valid `--token` or `--git-identity`."
@@ -87,8 +153,9 @@ def _get_repo_creds_and_default_branch_ssh(
87153
url: GitRepoURL, identity_file: PathLike, private_key: str
88154
) -> tuple[RemoteRepoCreds, Optional[str]]:
89155
_url = url.as_ssh()
156+
env = _make_git_env_for_creds_check(identity_file=identity_file)
90157
try:
91-
default_branch = _get_repo_default_branch(_url, make_git_env(identity_file=identity_file))
158+
default_branch = _get_repo_default_branch(_url, env)
92159
except GitCommandError as e:
93160
message = f"Cannot access `{_url}` using the `{identity_file}` private SSH key"
94161
raise InvalidRepoCredentialsError(message) from e
@@ -104,8 +171,9 @@ def _get_repo_creds_and_default_branch_https(
104171
url: GitRepoURL, oauth_token: Optional[str] = None
105172
) -> tuple[RemoteRepoCreds, Optional[str]]:
106173
_url = url.as_https()
174+
env = _make_git_env_for_creds_check()
107175
try:
108-
default_branch = _get_repo_default_branch(url.as_https(oauth_token), make_git_env())
176+
default_branch = _get_repo_default_branch(url.as_https(oauth_token), env)
109177
except GitCommandError as e:
110178
message = f"Cannot access `{_url}`"
111179
if oauth_token is not None:
@@ -120,9 +188,32 @@ def _get_repo_creds_and_default_branch_https(
120188
return creds, default_branch
121189

122190

191+
def _make_git_env_for_creds_check(identity_file: Optional[PathLike] = None) -> dict[str, str]:
192+
# Our goal is to check if _provided_ creds (if any) are correct, so we need to be sure that
193+
# only the provided creds are used, without falling back to any additional mechanisms.
194+
# To do this, we:
195+
# 1. Disable all configs to ignore any stored creds
196+
# 2. Disable askpass to avoid asking for creds interactively or fetching stored creds from
197+
# a non-interactive askpass helper (for example, VS Code sets GIT_ASKPASS to its own helper,
198+
# which silently provides creds to Git).
199+
return make_git_env(disable_config=True, disable_askpass=True, identity_file=identity_file)
200+
201+
123202
def _get_repo_default_branch(url: str, env: dict[str, str]) -> Optional[str]:
203+
# Git shipped by Apple with XCode is patched to support an additional config scope
204+
# above "system" called "xcode". There is no option in `git config list` to show this config,
205+
# but you can list the merged config (`git config list` without options) and then exclude
206+
# all settings listed in `git config list --{system,global,local,worktree}`.
207+
# As of time of writing, there are only two settings in the "xcode" config, one of which breaks
208+
# our "is repo public?" check, namely "credential.helper=osxkeychain".
209+
# As there is no way to disable "xcode" config (no env variable, no CLI option, etc.),
210+
# the only way to disable credential helper is to override this specific setting with an empty
211+
# string via command line argument: `git -c credential.helper= COMMAND [ARGS ...]`.
212+
# See: https://github.com/git/git/commit/3d4355712b9fe77a96ad4ad877d92dc7ff6e0874
213+
# See: https://gist.github.com/ChrisTollefson/ab9c0a5d1dd4dd615217345c6936a307
214+
_git = git.cmd.Git()(c="credential.helper=")
124215
# output example: "ref: refs/heads/dev\tHEAD\n545344f77c0df78367085952a97fc3a058eb4c65\tHEAD"
125-
output: str = git.cmd.Git().ls_remote("--symref", url, "HEAD", env=env)
216+
output: str = _git.ls_remote("--symref", url, "HEAD", env=env)
126217
for line in output.splitlines():
127218
# line format: `<oid> TAB <ref> LF`
128219
oid, _, ref = line.partition("\t")

src/dstack/_internal/utils/ssh.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,28 @@ def make_ssh_command_for_git(identity_file: PathLike) -> str:
5050
)
5151

5252

53-
def make_git_env(*, identity_file: Optional[PathLike] = None) -> dict[str, str]:
54-
env: dict[str, str] = {"GIT_TERMINAL_PROMPT": "0"}
53+
def make_git_env(
54+
*,
55+
disable_prompt: bool = True,
56+
disable_askpass: bool = False,
57+
disable_config: bool = False,
58+
identity_file: Optional[PathLike] = None,
59+
) -> dict[str, str]:
60+
env: dict[str, str] = {}
61+
if disable_prompt:
62+
# Fail with error instead of prompting on the terminal (e.g., when asking for
63+
# HTTP authentication)
64+
env["GIT_TERMINAL_PROMPT"] = "0"
65+
if disable_askpass:
66+
env["GIT_ASKPASS"] = ""
67+
env["SSH_ASKPASS"] = ""
68+
if disable_config:
69+
# Disable system-wide config (usually /etc/gitconfig)
70+
env["GIT_CONFIG_SYSTEM"] = os.devnull
71+
# Disable user (aka "global") config ($XDG_CONFIG_HOME/git/config or ~/.git/config)
72+
env["GIT_CONFIG_GLOBAL"] = os.devnull
73+
# Disable repo (aka "local") config (./.git/config)
74+
env["GIT_DIR"] = os.devnull
5575
if identity_file is not None:
5676
env["GIT_SSH_COMMAND"] = make_ssh_command_for_git(identity_file)
5777
return env

0 commit comments

Comments
 (0)