Skip to content

Commit 58e76cc

Browse files
authored
Bring back support for Git :ref and :depth used together (#13713)
An optimization to fetch only the about-to-be-used refs introduced when the :depth option was added broke support for specifying a SHA1 prefix as :ref. Such possibility was neither documented nor tested, but certainly used in the wild. This use case is now explicitly supported with a new test case. We maintain this fetch optimization whenever :branch, :tag or :depth is specified. We fetch all refs when :ref is used without :depth to be able to resolve short SHA1 refs, and require full commit hashes to use :ref and :depth together. When using :ref with :depth, a full SHA1 is required by Git and surfaced in our docs. We enforce it when validating the options, leaving room for longer hashes. This commit brings back two tests to cover the :ref and :depth functionality, and adds a new one to cover the validation mentioned above.
1 parent bc436c8 commit 58e76cc

File tree

3 files changed

+111
-17
lines changed

3 files changed

+111
-17
lines changed

lib/mix/lib/mix/scm/git.ex

+33-15
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ defmodule Mix.SCM.Git do
99

1010
@impl true
1111
def format(opts) do
12-
if rev = opts[:ref] || opts[:branch] || opts[:tag] do
12+
if rev = get_opts_rev(opts) do
1313
"#{redact_uri(opts[:git])} - #{rev}"
1414
else
1515
redact_uri(opts[:git])
@@ -126,17 +126,17 @@ defmodule Mix.SCM.Git do
126126
update_origin(opts[:git])
127127

128128
# Fetch external data
129-
branch_or_tag = opts[:branch] || opts[:tag]
129+
rev = get_lock_rev(opts[:lock], opts) || get_opts_rev(opts)
130130

131131
["--git-dir=.git", "fetch", "--force", "--quiet"]
132132
|> Kernel.++(progress_switch(git_version()))
133133
|> Kernel.++(tags_switch(opts[:tag]))
134134
|> Kernel.++(depth_switch(opts[:depth]))
135-
|> Kernel.++(if branch_or_tag, do: ["origin", branch_or_tag], else: [])
135+
|> Kernel.++(refspec_switch(opts, rev))
136136
|> git!()
137137

138138
# Migrate the Git repo
139-
rev = get_lock_rev(opts[:lock], opts) || opts[:ref] || branch_or_tag || default_branch()
139+
rev = rev || default_branch()
140140
git!(["--git-dir=.git", "checkout", "--quiet", rev])
141141

142142
if opts[:submodules] do
@@ -219,6 +219,15 @@ defmodule Mix.SCM.Git do
219219
["--depth=#{n}"]
220220
end
221221

222+
defp refspec_switch(_opts, nil), do: []
223+
224+
defp refspec_switch(opts, rev) do
225+
case Keyword.take(opts, [:depth, :branch, :tag]) do
226+
[_ | _] -> ["origin", rev]
227+
_ -> []
228+
end
229+
end
230+
222231
## Helpers
223232

224233
defp validate_git_options(opts) do
@@ -249,25 +258,30 @@ defmodule Mix.SCM.Git do
249258
end
250259
end
251260

261+
@sha1_size 40
262+
252263
defp validate_depth(opts) do
253-
case Keyword.take(opts, [:depth, :ref]) do
254-
[_, _] ->
255-
Mix.raise(
256-
"Cannot specify :depth and :ref at the same time. " <>
257-
"Error on Git dependency: #{redact_uri(opts[:git])}"
258-
)
264+
case Keyword.take(opts, [:depth]) do
265+
[] ->
266+
opts
267+
268+
[{:depth, depth}] when is_integer(depth) and depth > 0 ->
269+
ref = opts[:ref]
270+
271+
if ref && byte_size(ref) < @sha1_size do
272+
Mix.raise(
273+
"When :depth is used with :ref, a full commit hash is required. " <>
274+
"Error on Git dependency: #{redact_uri(opts[:git])}"
275+
)
276+
end
259277

260-
[depth: depth] when is_integer(depth) and depth > 0 ->
261278
opts
262279

263-
[depth: invalid_depth] ->
280+
invalid_depth ->
264281
Mix.raise(
265282
"The depth must be a positive integer, and be specified only once, got: #{inspect(invalid_depth)}. " <>
266283
"Error on Git dependency: #{redact_uri(opts[:git])}"
267284
)
268-
269-
_ ->
270-
opts
271285
end
272286
end
273287

@@ -296,6 +310,10 @@ defmodule Mix.SCM.Git do
296310
end
297311
end
298312

313+
defp get_opts_rev(opts) do
314+
opts[:branch] || opts[:ref] || opts[:tag]
315+
end
316+
299317
defp redact_uri(git) do
300318
case URI.parse(git) do
301319
%{userinfo: nil} -> git

lib/mix/lib/mix/tasks/deps.ex

+2-2
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ defmodule Mix.Tasks.Deps do
125125
* `:depth` *(since v1.17.0)* - creates a shallow clone of the Git repository,
126126
limiting the history to the specified number of commits. This can significantly
127127
improve clone speed for large repositories when full history is not needed.
128-
The value must be a positive integer, typically `1`. Cannot be used with the
129-
`:ref` option.
128+
The value must be a positive integer, typically `1`. When using `:depth` with
129+
`:ref`, a fully spelled hex object name (a 40-character SHA-1 hash) is required.
130130
131131
If your Git repository requires authentication, such as basic username:password
132132
HTTP authentication via URLs, it can be achieved via Git configuration, keeping

lib/mix/test/mix/tasks/deps.git_test.exs

+76
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,22 @@ defmodule Mix.Tasks.DepsGitTest do
478478
purge([GitRepo, GitRepo.MixProject])
479479
end
480480

481+
test "fetches with short ref" do
482+
[<<short_sha1::binary-size(8), _::binary>>, _ | _] = get_git_repo_revs("git_repo")
483+
484+
Process.put(:git_repo_opts, ref: short_sha1)
485+
486+
in_fixture("no_mixfile", fn ->
487+
Mix.Project.push(GitApp)
488+
489+
Mix.Tasks.Deps.Get.run([])
490+
message = "* Getting git_repo (#{fixture_path("git_repo")} - #{short_sha1})"
491+
assert_received {:mix_shell, :info, [^message]}
492+
493+
refute_received {:mix_shell, :error, _}
494+
end)
495+
end
496+
481497
describe "Git depth option" do
482498
@describetag :git_depth
483499

@@ -530,6 +546,66 @@ defmodule Mix.Tasks.DepsGitTest do
530546
end)
531547
end
532548

549+
test "with ref" do
550+
[sha1, _ | _] = get_git_repo_revs("git_repo")
551+
552+
Process.put(:git_repo_opts, depth: 1, ref: sha1)
553+
554+
in_fixture("no_mixfile", fn ->
555+
Mix.Project.push(GitApp)
556+
557+
Mix.Tasks.Deps.Get.run([])
558+
message = "* Getting git_repo (#{fixture_path("git_repo")} - #{sha1})"
559+
assert_received {:mix_shell, :info, [^message]}
560+
assert_shallow("deps/git_repo", 1)
561+
end)
562+
end
563+
564+
test "raises with short ref" do
565+
# When fetching, Git requires a fully spelled hex object name. We prevent
566+
# this failure mode by validating the ref length.
567+
#
568+
# If Git ever changes such that it can resolve a short ref in a shallow
569+
# fetch, we can update our docs and this test to reflect that.
570+
#
571+
# https://git-scm.com/docs/git-fetch#Documentation/git-fetch.txt-ltrefspecgt
572+
# https://stackoverflow.com/a/43136160
573+
574+
[<<short_sha1::binary-size(8), _::binary>>, _ | _] = get_git_repo_revs("git_repo")
575+
576+
Process.put(:git_repo_opts, depth: 1, ref: short_sha1)
577+
578+
in_fixture("no_mixfile", fn ->
579+
Mix.Project.push(GitApp)
580+
581+
exception = assert_raise Mix.Error, fn -> Mix.Tasks.Deps.Get.run([]) end
582+
583+
assert Exception.message(exception) =~ "a full commit hash is required"
584+
end)
585+
end
586+
587+
test "changing refspec updates retaining depth" do
588+
[last, first | _] = get_git_repo_revs("git_repo")
589+
590+
Process.put(:git_repo_opts, ref: first, depth: 1)
591+
592+
in_fixture("no_mixfile", fn ->
593+
Mix.Project.push(GitApp)
594+
595+
Mix.Tasks.Deps.Get.run([])
596+
message = "* Getting git_repo (#{fixture_path("git_repo")} - #{first})"
597+
assert_received {:mix_shell, :info, [^message]}
598+
assert_shallow("deps/git_repo", 1)
599+
assert File.read!("mix.lock") =~ first
600+
601+
# Change refspec
602+
update_dep(ref: last, depth: 1)
603+
Mix.Tasks.Deps.Get.run([])
604+
assert_shallow("deps/git_repo", 1)
605+
assert File.read!("mix.lock") =~ last
606+
end)
607+
end
608+
533609
test "removing depth retains shallow repository" do
534610
# For compatibility and simplicity, we follow Git's behavior and do not
535611
# attempt to unshallow an existing repository. This should not be a

0 commit comments

Comments
 (0)