Skip to content

Commit 091ce84

Browse files
mobileoverlordJosé Valim
authored andcommitted
add git sparse checkout (#5063)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
1 parent bbce69b commit 091ce84

File tree

5 files changed

+206
-13
lines changed

5 files changed

+206
-13
lines changed

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

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ defmodule Mix.SCM.Git do
2525
end
2626

2727
def accepts_options(_app, opts) do
28+
opts = sparse_opts(opts)
2829
cond do
2930
gh = opts[:github] ->
3031
opts
@@ -41,7 +42,9 @@ defmodule Mix.SCM.Git do
4142

4243
def checked_out?(opts) do
4344
# Are we inside a Git repository?
44-
File.regular?(Path.join(opts[:dest], ".git/HEAD"))
45+
git_dest(opts)
46+
|> Path.join(".git/HEAD")
47+
|> File.regular?
4548
end
4649

4750
def lock_status(opts) do
@@ -50,7 +53,7 @@ defmodule Mix.SCM.Git do
5053

5154
cond do
5255
lock_rev = get_lock_rev(lock, opts) ->
53-
File.cd!(opts[:dest], fn ->
56+
File.cd!(git_dest(opts), fn ->
5457
%{origin: origin, rev: rev} = get_rev_info()
5558
if get_lock_repo(lock) == origin and lock_rev == rev do
5659
:ok
@@ -77,19 +80,30 @@ defmodule Mix.SCM.Git do
7780
def checkout(opts) do
7881
assert_git!()
7982

80-
path = opts[:dest]
83+
path = git_dest(opts)
8184
location = opts[:git]
8285

8386
_ = File.rm_rf!(path)
84-
git!(~s(clone --no-checkout --progress "#{location}" "#{path}"))
8587

86-
File.cd! path, fn -> do_checkout(opts) end
88+
fun =
89+
if opts[:sparse] do
90+
sparse_check(git_version())
91+
File.mkdir_p!(path)
92+
fn -> init_sparse(opts) end
93+
else
94+
git!(~s(clone --no-checkout --progress "#{location}" "#{path}"))
95+
fn -> do_checkout(opts) end
96+
end
97+
98+
File.cd! path, fun
8799
end
88100

89101
def update(opts) do
90102
assert_git!()
91103

92-
File.cd! opts[:dest], fn ->
104+
File.cd! git_dest(opts), fn ->
105+
sparse_toggle(opts)
106+
93107
location = opts[:git]
94108
update_origin(location)
95109

@@ -102,24 +116,81 @@ defmodule Mix.SCM.Git do
102116
end
103117
end
104118

119+
defp sparse_opts(opts) do
120+
if opts[:sparse] do
121+
dest = Path.join(opts[:dest], opts[:sparse])
122+
opts
123+
|> Keyword.put(:git_dest, opts[:dest])
124+
|> Keyword.put(:dest, dest)
125+
else
126+
opts
127+
end
128+
end
129+
130+
defp sparse_check(version) do
131+
unless {1, 7, 0} <= version do
132+
version =
133+
version
134+
|> Tuple.to_list
135+
|> Enum.join(".")
136+
Mix.raise "Git >= 1.7.0 is required to use sparse checkout. " <>
137+
"You are running version #{version}"
138+
end
139+
end
140+
141+
defp sparse_toggle(opts) do
142+
git!("config core.sparsecheckout #{opts[:sparse] != nil}")
143+
end
144+
145+
105146
defp progress_switch(version) when {1, 7, 1} <= version, do: " --progress"
106147
defp progress_switch(_), do: ""
107148

108149
defp tags_switch(nil), do: ""
109150
defp tags_switch(_), do: " --tags"
110151

152+
defp git_dest(opts) do
153+
if opts[:git_dest] do
154+
opts[:git_dest]
155+
else
156+
opts[:dest]
157+
end
158+
end
159+
111160
## Helpers
112161

113162
defp validate_git_options(opts) do
114-
case Keyword.take(opts, [:branch, :ref, :tag]) do
115-
[] -> opts
163+
err = "You should specify only one of branch, ref or tag, and only once. " <>
164+
"Error on Git dependency: #{opts[:git]}"
165+
validate_single_uniq(opts, [:branch, :ref, :tag], err)
166+
167+
err = "You should specify only one sparse path. " <>
168+
"Error on Git dependency: #{opts[:git]}"
169+
validate_single_uniq(opts, [:sparse], err)
170+
end
171+
172+
defp validate_single_uniq(opts, take, error) do
173+
case Keyword.take(opts, take) do
174+
[] -> opts
116175
[_] -> opts
117-
_ ->
118-
Mix.raise "You should specify only one of branch, ref or tag, and only once. " <>
119-
"Error on Git dependency: #{opts[:git]}"
176+
_ -> Mix.raise error
120177
end
121178
end
122179

180+
defp init_sparse(opts) do
181+
git!("init --quiet")
182+
git!("remote add origin #{opts[:git]} --fetch")
183+
sparse_toggle(opts)
184+
185+
sparse_info =
186+
File.cwd!
187+
|> Path.join(".git/info/sparse-checkout")
188+
189+
File.write(sparse_info, opts[:sparse])
190+
191+
do_checkout(opts)
192+
end
193+
123194
defp do_checkout(opts) do
124195
rev = get_lock_rev(opts[:lock], opts) || get_opts_rev(opts)
125196
git!("--git-dir=.git checkout --quiet #{rev}")
@@ -147,6 +218,13 @@ defmodule Mix.SCM.Git do
147218

148219
defp get_lock_opts(opts) do
149220
lock_opts = Keyword.take(opts, [:branch, :ref, :tag])
221+
lock_opts =
222+
if opts[:sparse] do
223+
lock_opts ++ [sparse: opts[:sparse]]
224+
else
225+
lock_opts
226+
end
227+
150228
if opts[:submodules] do
151229
lock_opts ++ [submodules: true]
152230
else

lib/mix/test/fixtures/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
git_repo
2+
git_sparse_repo
23
deps_on_git_repo
34
git_rebar

lib/mix/test/mix/scm/git_test.exs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ defmodule Mix.SCM.GitTest do
3030
assert_raise Mix.Error, ~r/You should specify only one of branch, ref or tag/, fn ->
3131
Mix.SCM.Git.accepts_options(nil, [git: "/repo", branch: "master", branch: "develop"])
3232
end
33+
34+
assert_raise Mix.Error, ~r/You should specify only one sparse path/, fn ->
35+
Mix.SCM.Git.accepts_options(nil, [git: "/repo", sparse: "/a", sparse: "/b", dest: "/repo"])
36+
end
3337
end
3438

3539
defp lock(opts \\ []) do

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

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ defmodule Mix.Tasks.DepsGitTest do
3434
end
3535
end
3636

37+
defmodule GitSparseApp do
38+
def project do
39+
[app: :git_app,
40+
version: "0.1.0",
41+
deps: [
42+
{:git_sparse_repo, "0.1.0", git: fixture_path("git_sparse_repo"), sparse: "sparse_dir"}
43+
]]
44+
end
45+
end
46+
3747
defmodule GitErrorApp do
3848
def project do
3949
[deps: [
@@ -69,6 +79,17 @@ defmodule Mix.Tasks.DepsGitTest do
6979
end
7080
end
7181

82+
test "gets and updates Git repos with sparse checkout" do
83+
Mix.Project.push GitSparseApp
84+
85+
in_fixture "no_mixfile", fn ->
86+
Mix.Tasks.Deps.Get.run []
87+
message = "* Getting git_sparse_repo (#{fixture_path("git_sparse_repo")})"
88+
assert_received {:mix_shell, :info, [^message]}
89+
assert File.read!("mix.lock") =~ "sparse: \"sparse_dir\""
90+
end
91+
end
92+
7293
test "handles invalid .git directory" do
7394
Mix.Project.push GitApp
7495

@@ -265,6 +286,41 @@ defmodule Mix.Tasks.DepsGitTest do
265286
purge [GitRepo, GitRepo.Mixfile]
266287
end
267288

289+
# sparse
290+
test "updates the repo when sparse changes" do
291+
Mix.Project.push GitSparseApp
292+
[ref | _] = get_git_repo_revs("git_sparse_repo")
293+
294+
in_fixture "no_mixfile", fn ->
295+
Mix.Dep.Lock.write %{git_sparse_repo: {:git, fixture_path("git_sparse_repo"), ref, [sparse: "sparse_dir"]}}
296+
297+
Mix.Tasks.Deps.Get.run []
298+
299+
# Update the lock and now we should get an error
300+
Mix.Dep.Lock.write %{git_sparse_repo: {:git, fixture_path("git_sparse_repo"), ref, []}}
301+
assert_raise Mix.Error, fn ->
302+
Mix.Tasks.Deps.Loadpaths.run []
303+
end
304+
305+
# # Flush the errors we got, move to a clean slate
306+
# Mix.shell.flush
307+
# Mix.Task.clear
308+
#
309+
# # Calling get should update the dependency
310+
# Mix.Tasks.Deps.Get.run []
311+
# assert File.exists?("deps/git_repo/lib/git_repo.ex")
312+
# assert File.read!("mix.lock") =~ last
313+
#
314+
# message = "* Updating git_repo (#{fixture_path("git_repo")})"
315+
# assert_received {:mix_shell, :info, [^message]}
316+
#
317+
# # Check we got no error
318+
# refute_received {:mix_shell, :error, _}
319+
end
320+
after
321+
purge [GitSparseRepo, GitSparseRepo.Mixfile]
322+
end
323+
268324
test "updates the repo and the lock when the mixfile updates" do
269325
Mix.Project.push GitApp
270326
[last, first | _] = get_git_repo_revs
@@ -364,8 +420,8 @@ defmodule Mix.Tasks.DepsGitTest do
364420
Mix.Project.push(name, file)
365421
end
366422

367-
defp get_git_repo_revs do
368-
File.cd! fixture_path("git_repo"), fn ->
423+
defp get_git_repo_revs(fixture \\ "git_repo") do
424+
File.cd! fixture_path(fixture), fn ->
369425
Regex.split ~r(\r?\n), System.cmd("git", ["log", "--format=%H"]) |> elem(0)
370426
end
371427
end

lib/mix/test/test_helper.exs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,60 @@ unless File.dir?(target) do
237237
end
238238
end
239239

240+
# Git Sparse
241+
target = Path.expand("fixtures/git_sparse_repo", __DIR__)
242+
243+
unless File.dir?(target) do
244+
subdir = Path.join(target, "sparse_dir")
245+
246+
File.mkdir_p!(Path.join(subdir, "lib"))
247+
248+
File.write! Path.join(subdir, "mix.exs"), """
249+
## Auto-generated fixture
250+
raise "I was not supposed to be loaded"
251+
"""
252+
253+
File.cd! target, fn ->
254+
System.cmd("git", ~w[init])
255+
System.cmd("git", ~w[config user.email "mix@example.com"])
256+
System.cmd("git", ~w[config user.name "mix-repo"])
257+
System.cmd("git", ~w[add .])
258+
System.cmd("git", ~w[commit -m "bad"])
259+
end
260+
261+
File.write! Path.join(subdir, "mix.exs"), """
262+
## Auto-generated fixture
263+
defmodule GitSparseRepo.Mixfile do
264+
use Mix.Project
265+
266+
def project do
267+
[app: :git_sparse_repo, version: "0.1.0"]
268+
end
269+
end
270+
"""
271+
272+
File.cd! target, fn ->
273+
System.cmd("git", ~w[add .])
274+
System.cmd("git", ~w[commit -m "ok"])
275+
System.cmd("git", ~w[tag without_module])
276+
end
277+
278+
File.write! Path.join(subdir, "lib/git_sparse_repo.ex"), """
279+
## Auto-generated fixture
280+
defmodule GitSparseRepo do
281+
def hello do
282+
"World"
283+
end
284+
end
285+
"""
286+
287+
File.cd! target, fn ->
288+
System.cmd("git", ~w[add .])
289+
System.cmd("git", ~w[commit -m "lib"])
290+
System.cmd("git", ~w[tag with_module])
291+
end
292+
end
293+
240294
# Deps on Git repo
241295
target = Path.expand("fixtures/deps_on_git_repo", __DIR__)
242296

0 commit comments

Comments
 (0)