Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions lib/exgit/fs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,13 @@ defmodule Exgit.FS do

@doc """
Write `content` to `path`. Returns `{:ok, new_tree_sha, repo}`.

An unborn base — a ref *name* with no entry in the ref store, as on a
freshly initialized or cloned-empty repository — starts from the empty
tree, mirroring how `Exgit.Workspace.commit/2` treats an unborn ref as
parentless. A raw SHA that fails to resolve stays `{:error, :not_found}`:
in a lazy clone that is a missing object, and starting from empty there
would silently drop every existing entry from the produced tree.
"""
@spec write_path(Repository.t(), ref(), path(), binary(), keyword()) ::
{:ok, binary(), Repository.t()} | {:error, term()}
Expand All @@ -1254,16 +1261,41 @@ defmodule Exgit.FS do
if segments == [] do
{:error, :cannot_write_root}
else
with {:ok, tree_sha, repo} <- resolve_tree(repo, reference) do
with {:ok, base, repo} <- resolve_base_tree_for_write(repo, reference) do
blob = Blob.new(content)
{:ok, blob_sha, store} = ObjectStore.put(repo.object_store, blob)
repo = %{repo | object_store: store}

insert_blob_into_tree(repo, tree_sha, segments, mode, blob_sha)
case base do
{:tree, tree_sha} -> insert_blob_into_tree(repo, tree_sha, segments, mode, blob_sha)
:unborn -> insert_blob_into_empty(repo, segments, mode, blob_sha)
end
end
end
end

defp resolve_base_tree_for_write(repo, reference) do
case resolve_tree(repo, reference) do
{:ok, tree_sha, repo} ->
{:ok, {:tree, tree_sha}, repo}

{:error, :not_found} = err ->
if unborn_ref?(repo, reference), do: {:ok, :unborn, repo}, else: err

{:error, _} = err ->
err
end
end

# Unborn means the reference is a ref name (per the same heuristic
# resolve_tree/2 uses to tell names from raw SHAs) that the ref store
# has no entry for. A ref that resolves to a SHA whose object is
# missing is NOT unborn — that is a lazy-clone fetch miss.
defp unborn_ref?(repo, reference) do
ref_name? = byte_size(reference) != 20 or printable_ascii_ref?(reference)
ref_name? and match?({:error, :not_found}, RefStore.resolve(repo.ref_store, reference))
end

defp insert_blob_into_tree(repo, tree_sha, [name], mode, blob_sha) do
with {:ok, %Tree{entries: entries}, repo} <- fetch_object(repo, tree_sha) do
new_entries =
Expand Down
39 changes: 39 additions & 0 deletions test/exgit/workspace_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,42 @@ defmodule Exgit.WorkspaceTest do
end
end
end

defmodule Exgit.WorkspaceUnbornRefTest do
use ExUnit.Case, async: true

alias Exgit.Workspace

describe "workspace over an unborn branch" do
test "write starts from the empty tree; commit and read work" do
{:ok, repo} = Exgit.init()
ws = Workspace.open(repo, "refs/heads/main")

assert {:ok, ws} = Workspace.write(ws, "/hello.txt", "hi\n")

assert {:ok, _sha, ws} =
Workspace.commit(ws,
message: "first",
author: %{name: "t", email: "t@t"},
update_ref: "refs/heads/main"
)

assert {:ok, "hi\n", _ws} = Workspace.read(ws, "/hello.txt")
end

test "nested paths build intermediate trees from nothing" do
{:ok, repo} = Exgit.init()
ws = Workspace.open(repo, "refs/heads/main")

assert {:ok, ws} = Workspace.write(ws, "/a/b/c.txt", "deep\n")
assert {:ok, "deep\n", _ws} = Workspace.read(ws, "/a/b/c.txt")
end

test "a missing raw SHA base is still an error, not an empty tree" do
{:ok, repo} = Exgit.init()
ws = Workspace.open(repo, <<0::160>>)

assert {:error, :not_found} = Workspace.write(ws, "/hello.txt", "hi\n")
end
end
end
Loading