Apply review comments

This commit is contained in:
Alexandre de Souza 2025-09-11 13:59:40 -03:00
parent fe2dda96f8
commit 6161ef62df
No known key found for this signature in database
GPG key ID: E39228FFBA346545
5 changed files with 60 additions and 71 deletions

View file

@ -83,7 +83,7 @@ RUN apt-get update && apt-get upgrade -y && \
# In case someone uses `Mix.install/2` and point to a git repo # In case someone uses `Mix.install/2` and point to a git repo
git \ git \
# In case someone uses the Git file storage # In case someone uses the Git file storage
ssh \ openssh-client \
# Additional standard tools # Additional standard tools
wget \ wget \
# In case someone uses Torchx for Nx # In case someone uses Torchx for Nx

View file

@ -1,6 +1,26 @@
defmodule Livebook.FileSystem.Git.Client do defmodule Livebook.FileSystem.Git.Client do
alias Livebook.FileSystem alias Livebook.FileSystem
@doc """
Clones the given repository to your local file system.
"""
@spec init(FileSystem.Git.t()) :: :ok | {:error, FileSystem.error()}
def init(%FileSystem.Git{} = file_system) do
case fetch_repository(file_system) do
{:ok, _} ->
:ok
{:error, _reason} ->
git_dir = FileSystem.Git.git_dir(file_system)
with_ssh_key_file(file_system, fn key_path ->
with {:ok, _} <- clone(git_dir, file_system.repo_url, key_path) do
fetch(git_dir, file_system.branch, key_path)
end
end)
end
end
@doc """ @doc """
Returns a list of files from given repository and given path. Returns a list of files from given repository and given path.
""" """
@ -50,31 +70,12 @@ defmodule Livebook.FileSystem.Git.Client do
end end
@doc """ @doc """
Fetches the repository with latest changes and checkout branch. Fetches the repository with latest changes from branch.
""" """
@spec fetch(FileSystem.Git.t()) :: :ok | {:error, FileSystem.error()} @spec fetch(FileSystem.Git.t()) :: :ok | {:error, FileSystem.error()}
def fetch(%FileSystem.Git{} = file_system) do def fetch(%FileSystem.Git{} = file_system) do
with {:ok, git_dir} <- fetch_repository(file_system) do with {:ok, git_dir} <- fetch_repository(file_system) do
with_ssh_key_file(file_system, fn key_path -> with_ssh_key_file(file_system, &fetch(git_dir, file_system.branch, &1))
fetch(git_dir, file_system.branch, key_path)
end)
end
end
@doc """
Removes the repository locally.
"""
@spec remove_repository(FileSystem.Git.t()) :: :ok | {:error, FileSystem.error()}
def remove_repository(%FileSystem.Git{} = file_system) do
git_dir = FileSystem.Git.git_dir(file_system)
key_path = FileSystem.Git.key_path(file_system)
with {:ok, _} <- File.rm_rf(git_dir),
:ok <- File.rm(key_path) do
:ok
else
{:error, reason, _file} -> FileSystem.Utils.posix_error(reason)
error -> error
end end
end end
@ -100,15 +101,15 @@ defmodule Livebook.FileSystem.Git.Client do
end end
defp clone(git_dir, repo_url, key_path) do defp clone(git_dir, repo_url, key_path) do
with {:ok, ssh} <- fetch_executable("ssh") do with {:ok, _ssh} <- fetch_executable("ssh") do
git(git_dir, ["clone", "--bare", "--depth=1", repo_url, git_dir], env_opts(ssh, key_path)) git(git_dir, ["clone", "--bare", "--depth=1", repo_url, git_dir], env_opts(key_path))
end end
end end
defp fetch(git_dir, branch, key_path) do defp fetch(git_dir, branch, key_path) do
with {:ok, ssh} <- fetch_executable("ssh"), with {:ok, _ssh} <- fetch_executable("ssh"),
{:ok, _} <- {:ok, _} <-
git(git_dir, ["fetch", "origin", "#{branch}:#{branch}"], env_opts(ssh, key_path)) do git(git_dir, ["fetch", "origin", "#{branch}:#{branch}"], env_opts(key_path)) do
:ok :ok
end end
end end
@ -117,12 +118,12 @@ defmodule Livebook.FileSystem.Git.Client do
with {:ok, git} <- fetch_executable("git") do with {:ok, git} <- fetch_executable("git") do
case System.cmd(git, args, cmd_opts(git_dir, opts)) do case System.cmd(git, args, cmd_opts(git_dir, opts)) do
{result, 0} -> {:ok, result} {result, 0} -> {:ok, result}
{error, _} -> {:error, String.trim(error)} {error, _} -> {:error, normalize_error_message(error)}
end end
end end
end end
@cmd_opts [use_stdio: true, stderr_to_stdout: true] @cmd_opts [stderr_to_stdout: true]
defp cmd_opts(git_dir, opts) do defp cmd_opts(git_dir, opts) do
opts = Keyword.merge(@cmd_opts, opts) opts = Keyword.merge(@cmd_opts, opts)
@ -134,8 +135,8 @@ defmodule Livebook.FileSystem.Git.Client do
end end
end end
defp env_opts(ssh, key_path) do defp env_opts(key_path) do
[env: %{"GIT_SSH_COMMAND" => "#{ssh} -i #{key_path}"}] [env: %{"GIT_SSH_COMMAND" => "ssh -i '#{key_path}'"}]
end end
defp fetch_repository(file_system) do defp fetch_repository(file_system) do
@ -144,30 +145,19 @@ defmodule Livebook.FileSystem.Git.Client do
if File.exists?(git_dir) do if File.exists?(git_dir) do
{:ok, git_dir} {:ok, git_dir}
else else
with_ssh_key_file(file_system, fn key_path -> {:error, "repository not found"}
with {:ok, _} <- clone(git_dir, file_system.repo_url, key_path) do
{:ok, git_dir}
end
end)
end end
end end
@begin_key "-----BEGIN OPENSSH PRIVATE KEY-----"
@end_key "-----END OPENSSH PRIVATE KEY-----"
defp with_ssh_key_file(file_system, fun) when is_function(fun, 1) do defp with_ssh_key_file(file_system, fun) when is_function(fun, 1) do
File.mkdir_p!(FileSystem.Git.ssh_path()) File.mkdir_p!(FileSystem.Git.ssh_path())
key_path = FileSystem.Git.key_path(file_system) key_path = FileSystem.Git.key_path(file_system)
pem_entry = :public_key.pem_decode(file_system.key)
ssh_key = :public_key.pem_encode(pem_entry)
if not File.exists?(key_path) do File.write!(key_path, ssh_key)
File.write!(key_path, """ File.chmod!(key_path, 0o600)
#{@begin_key}
#{normalize_ssh_key(file_system.key)}
#{@end_key}
""")
File.chmod!(key_path, 0o600)
end
result = fun.(key_path) result = fun.(key_path)
@ -178,21 +168,27 @@ defmodule Livebook.FileSystem.Git.Client do
result result
end end
defp normalize_ssh_key(key) do defp normalize_error_message("Cloning" <> _ = error) do
key [_, error] = String.split(error, "Permission denied", trim: true)
|> String.replace_prefix(@begin_key, "")
|> String.replace_suffix(@end_key, "") ("Permission denied" <> error)
|> String.replace("\r\n", "\s")
|> String.replace("\n\n", "\s")
|> String.replace("\n", "\s")
|> String.replace("fatal: ", "")
|> String.trim() |> String.trim()
|> String.split("\s")
|> Enum.join("\n")
end end
defp normalize_dir_path(<<"blob ", path::binary>>), do: normalize_path(path) defp normalize_error_message(error) do
defp normalize_dir_path(<<"tree ", path::binary>>), do: normalize_path(path <> "/") [error] = String.split(error, "fatal: ", trim: true)
# avoid MatchError when it has only one part
[error | _] = String.split(error, "\n", trim: true)
defp normalize_path("/"), do: "." String.trim(error)
defp normalize_path("/" <> _ = path), do: path end
defp normalize_path(path), do: "/" <> path
defp normalize_dir_path(<<"blob ", path::binary>>), do: "/" <> path
defp normalize_dir_path(<<"tree ", path::binary>>), do: "/" <> path <> "/"
defp relative_path("/"), do: "." defp relative_path("/"), do: "."
defp relative_path("/" <> path), do: path defp relative_path("/" <> path), do: path

View file

@ -22,7 +22,6 @@ defmodule LivebookWeb.Hub.FileSystemFormComponent do
{:ok, {:ok,
socket socket
|> assign_new(:hub, fn -> assigns.hub end) |> assign_new(:hub, fn -> assigns.hub end)
|> assign_new(:teams?, fn -> assigns.hub.id != Livebook.Hubs.Personal.id() end)
|> assign_new(:file_system, fn -> file_system end) |> assign_new(:file_system, fn -> file_system end)
|> assign_new(:type, fn -> FileSystems.type(file_system) end) |> assign_new(:type, fn -> FileSystems.type(file_system) end)
|> assign_new(:mode, fn -> mode end) |> assign_new(:mode, fn -> mode end)
@ -41,17 +40,11 @@ defmodule LivebookWeb.Hub.FileSystemFormComponent do
{@title} {@title}
</h3> </h3>
<p :if={not @teams?} class="text-gray-700">
Configure an AWS S3 bucket as a Livebook file storage.
Many storage services offer an S3-compatible API and
those work as well.
</p>
<div :if={@error_message} class="error-box"> <div :if={@error_message} class="error-box">
{@error_message} {@error_message}
</div> </div>
<div :if={@teams?}> <div>
<label class="mb-2 flex items-center gap-1 text-sm text-gray-800 font-medium"> <label class="mb-2 flex items-center gap-1 text-sm text-gray-800 font-medium">
Type Type
</label> </label>
@ -146,7 +139,7 @@ defmodule LivebookWeb.Hub.FileSystemFormComponent do
""" """
end end
defp file_system_form_fields(%{file_system: %FileSystem.Git{}, teams?: true} = assigns) do defp file_system_form_fields(%{file_system: %FileSystem.Git{}} = assigns) do
~H""" ~H"""
<div class="flex flex-col space-y-4"> <div class="flex flex-col space-y-4">
<.text_field <.text_field

View file

@ -45,7 +45,7 @@ defmodule Livebook.FileSystem.GitTest do
describe "FileSystem.read/2" do describe "FileSystem.read/2" do
test "returns an error when a nonexistent key is given", %{file_system: file_system} do test "returns an error when a nonexistent key is given", %{file_system: file_system} do
assert FileSystem.read(file_system, "/another_file.txt") == assert FileSystem.read(file_system, "/another_file.txt") ==
{:error, "fatal: path 'another_file.txt' does not exist in 'main'"} {:error, "path 'another_file.txt' does not exist in 'main'"}
end end
test "returns object contents under the given key", %{file_system: file_system} do test "returns object contents under the given key", %{file_system: file_system} do
@ -97,7 +97,7 @@ defmodule Livebook.FileSystem.GitTest do
describe "FileSystem.etag_for/2" do describe "FileSystem.etag_for/2" do
test "returns an error when a nonexistent key is given", %{file_system: file_system} do test "returns an error when a nonexistent key is given", %{file_system: file_system} do
assert {:error, reason} = FileSystem.etag_for(file_system, "/another_file.txt") assert {:error, reason} = FileSystem.etag_for(file_system, "/another_file.txt")
assert reason =~ "fatal: path 'another_file.txt' does not exist in 'main'" assert reason =~ "path 'another_file.txt' does not exist in 'main'"
end end
test "returns the ETag value received from the server", %{file_system: file_system} do test "returns the ETag value received from the server", %{file_system: file_system} do
@ -112,7 +112,7 @@ defmodule Livebook.FileSystem.GitTest do
end end
test "returns error with invalid path", %{file_system: file_system} do test "returns error with invalid path", %{file_system: file_system} do
assert {:error, "fatal: ../../.bashrc: '../../.bashrc' is outside repository at" <> _} = assert {:error, "../../.bashrc: '../../.bashrc' is outside repository at" <> _} =
FileSystem.exists?(file_system, "../../.bashrc") FileSystem.exists?(file_system, "../../.bashrc")
end end
end end

View file

@ -44,7 +44,7 @@ defmodule LivebookWeb.Integration.OpenLiveTest do
|> element(~s{button[id*="file-system-#{file_system.id}"]}) |> element(~s{button[id*="file-system-#{file_system.id}"]})
|> render_click() |> render_click()
# guarantee the write functions were disabled # guarantee the write functions are disabled
assert has_element?(view, ~s{div[id*="new-item-menu"] button[disabled]}) assert has_element?(view, ~s{div[id*="new-item-menu"] button[disabled]})
assert render(view) =~ "notebook_files" assert render(view) =~ "notebook_files"