Current section

Files

Jump to
livebook lib livebook file_system git client.ex
Raw

lib/livebook/file_system/git/client.ex

defmodule Livebook.FileSystem.Git.Client do
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 """
Returns a list of files from given repository and given path.
"""
@spec list_files(FileSystem.Git.t(), String.t()) ::
{:ok, list(String.t())} | {:error, FileSystem.error()}
def list_files(%FileSystem.Git{} = file_system, path) do
path = relative_path(path)
with {:ok, git_dir} <- fetch_repository(file_system) do
ls_tree(git_dir, file_system.branch, path)
end
end
@doc """
Returns the content of the given file from given repository.
"""
@spec read_file(FileSystem.Git.t(), String.t()) ::
{:ok, String.t()} | {:error, FileSystem.error()}
def read_file(%FileSystem.Git{} = file_system, path) do
path = relative_path(path)
with {:ok, git_dir} <- fetch_repository(file_system) do
git(git_dir, ["show", "#{file_system.branch}:#{path}"])
end
end
@doc """
Streams the content of the given file from given repository.
"""
@spec stream_file(FileSystem.Git.t(), String.t(), Collectable.t()) ::
{:ok, String.t()} | {:error, FileSystem.error()}
def stream_file(%FileSystem.Git{} = file_system, path, collectable) do
path = relative_path(path)
with {:ok, git_dir} <- fetch_repository(file_system) do
git(git_dir, ["show", "#{file_system.branch}:#{path}"], into: collectable)
end
end
@doc """
Returns the ETag of the given file from given repository.
"""
@spec etag(FileSystem.Git.t(), String.t()) :: {:ok, String.t()} | {:error, FileSystem.error()}
def etag(%FileSystem.Git{} = file_system, path) do
path = relative_path(path)
with {:ok, git_dir} <- fetch_repository(file_system) do
rev_parse(git_dir, file_system.branch, path)
end
end
@doc """
Returns if the given path exists in given repository.
"""
@spec exists?(FileSystem.Git.t(), String.t()) :: {:ok, boolean()} | {:error, FileSystem.error()}
def exists?(%FileSystem.Git{} = file_system, path) do
with {:ok, keys} <- list_files(file_system, path) do
{:ok, keys != []}
end
end
@doc """
Fetches the repository with latest changes from branch.
"""
@spec fetch(FileSystem.Git.t()) :: :ok | {:error, FileSystem.error()}
def fetch(%FileSystem.Git{} = file_system) do
with {:ok, git_dir} <- fetch_repository(file_system) do
with_ssh_key_file(file_system, &fetch(git_dir, file_system.branch, &1))
end
end
@ls_tree_format "--format=%(objecttype) %(path)"
defp ls_tree(git_dir, branch, path) do
with {:ok, result} <- git(git_dir, ["ls-tree", @ls_tree_format, branch, path]) do
{:ok,
result
|> String.split("\n", trim: true)
|> Enum.map(&normalize_dir_path/1)}
end
end
defp rev_parse(git_dir, branch, path) do
with {:ok, etag} <- git(git_dir, ["rev-parse", "#{branch}:#{path}"]) do
{:ok, String.trim(etag)}
end
end
defp clone(git_dir, repo_url, key_path) do
with {:ok, _ssh} <- fetch_executable("ssh") do
git(git_dir, ["clone", "--bare", "--depth=1", repo_url, git_dir], env_opts(key_path))
end
end
defp fetch(git_dir, branch, key_path) do
with {:ok, _ssh} <- fetch_executable("ssh"),
{:ok, _} <-
git(git_dir, ["fetch", "origin", "#{branch}:#{branch}"], env_opts(key_path)) do
:ok
end
end
defp git(git_dir, args, opts \\ []) do
with {:ok, git} <- fetch_executable("git") do
case System.cmd(git, args, cmd_opts(git_dir, opts)) do
{result, 0} -> {:ok, result}
{error, _} -> {:error, String.trim(error)}
end
end
end
@cmd_opts [stderr_to_stdout: true]
defp cmd_opts(git_dir, opts) do
opts = Keyword.merge(@cmd_opts, opts)
if File.exists?(git_dir) do
Keyword.merge(opts, cd: git_dir)
else
opts
end
end
defp env_opts(key_path) do
[env: %{"GIT_SSH_COMMAND" => "ssh -o StrictHostKeyChecking=no -i '#{key_path}'"}]
end
defp fetch_repository(file_system) do
git_dir = FileSystem.Git.git_dir(file_system)
if File.exists?(git_dir) do
{:ok, git_dir}
else
{:error, "repository not found"}
end
end
defp with_ssh_key_file(file_system, fun) when is_function(fun, 1) do
File.mkdir_p!(Livebook.Config.tmp_path())
key_path = FileSystem.Git.key_path(file_system)
ssh_key =
file_system.key
|> String.replace(
~r/ (?!(?:OPENSSH |RSA |EC |DSA |PRIVATE )?(?:PRIVATE )?KEY-----)/,
"\n"
)
|> :public_key.pem_decode()
|> :public_key.pem_encode()
File.write!(key_path, ssh_key)
File.chmod!(key_path, 0o600)
result = fun.(key_path)
if File.exists?(key_path) do
File.rm!(key_path)
end
result
end
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("/" <> path), do: path
defp relative_path(path), do: path
defp fetch_executable(program) do
if bin = System.find_executable(program) do
{:ok, bin}
else
{:error, "'#{program}' executable not found"}
end
end
end