diff --git a/lib/xgit/repository/working_tree.ex b/lib/xgit/repository/working_tree.ex index 436d6e6..77fce7c 100644 --- a/lib/xgit/repository/working_tree.ex +++ b/lib/xgit/repository/working_tree.ex @@ -23,6 +23,8 @@ defmodule Xgit.Repository.WorkingTree do alias Xgit.Core.DirCache alias Xgit.Core.DirCache.Entry, as: DirCacheEntry alias Xgit.Core.FilePath + alias Xgit.Core.Object + alias Xgit.Core.ObjectId alias Xgit.Repository alias Xgit.Repository.WorkingTree.ParseIndexFile alias Xgit.Repository.WorkingTree.WriteIndexFile @@ -242,6 +244,123 @@ defmodule Xgit.Repository.WorkingTree do end end + @typedoc ~S""" + Reason codes that can be returned by `write_tree/2`. + """ + @type write_tree_reason :: + :incomplete_merge + | :objects_missing + | :prefix_not_found + | DirCache.to_tree_objects_reason() + | ParseIndexFile.from_iodevice_reason() + | Repository.put_loose_object_reason() + + @doc ~S""" + Translates the current dir cache, as reflected in its index file, to one or more + tree objects. + + The working tree must be in a fully-merged state. + + ## Options + + `:missing_ok?`: `true` to ignore any objects that are referenced by the index + file that are not present in the object database. Normally this would be an error. + + `:prefix`: (`Xgit.Core.FilePath`) if present, returns the `object_id` for the tree at + the given subdirectory. If not present, writes a tree corresponding to the root. + (The entire tree is written in either case.) + + ## Return Value + + `{:ok, object_id}` with the object ID for the tree that was generated. (If the exact tree + specified by the index already existed, it will return that existing tree's ID.) + + `{:error, :incomplete_merge}` if any entry in the index file is not fully merged. + + `{:error, :objects_missing}` if any of the objects referenced by the index + are not present in the object store. (Exception: If `missing_ok?` is `true`, + then this condition will be ignored.) + + `{:error, :prefix_not_found}` if `prefix` was specified, but that prefix is not referenced + in the index file. + + Reason codes may also come from the following functions: + + * `Xgit.Core.DirCache.to_tree_objects/2` + * `Xgit.Repository.put_loose_object/2` + * `Xgit.Repository.WorkingTree.ParseIndexFile.from_iodevice/1` + """ + @spec write_tree(working_tree :: t, missing_ok?: boolean, prefix: FilePath.t()) :: + {:ok, object_id :: ObjectId.t()} | {:error, reason :: write_tree_reason} + def write_tree(working_tree, opts \\ []) when is_pid(working_tree) do + {missing_ok?, prefix} = validate_options(opts) + GenServer.call(working_tree, {:write_tree, missing_ok?, prefix}) + end + + defp validate_options(opts) do + missing_ok? = Keyword.get(opts, :missing_ok?, false) + + unless is_boolean(missing_ok?) do + raise ArgumentError, + "Xgit.Repository.WorkingTree.write_tree/2: missing_ok? #{inspect(missing_ok?)} is invalid" + end + + prefix = Keyword.get(opts, :prefix, []) + + unless prefix == [] or FilePath.valid?(prefix) do + raise ArgumentError, + "Xgit.Repository.WorkingTree.write_tree/2: prefix #{inspect(prefix)} is invalid (should be a charlist, not a String)" + end + + {missing_ok?, prefix} + end + + defp handle_write_tree( + missing_ok?, + prefix, + %{repository: repository, work_dir: work_dir} = state + ) do + with {:ok, %DirCache{entries: entries} = dir_cache} <- parse_dir_cache_if_exists(work_dir), + {:merged?, true} <- {:merged?, DirCache.fully_merged?(dir_cache)}, + {:has_all_objects?, true} <- + {:has_all_objects?, has_all_objects?(repository, entries, missing_ok?)}, + {:ok, objects, %Object{id: object_id}} <- DirCache.to_tree_objects(dir_cache, prefix), + :ok <- write_all_objects(repository, objects) do + cover {:reply, {:ok, object_id}, state} + else + {:error, reason} -> cover {:reply, {:error, reason}, state} + {:merged?, false} -> cover {:reply, {:error, :incomplete_merge}, state} + {:has_all_objects?, false} -> cover {:reply, {:error, :objects_missing}, state} + end + end + + defp has_all_objects?(repository, entries, missing_ok?) + + defp has_all_objects?(_repository, _entries, true), do: cover(true) + + defp has_all_objects?(repository, entries, false) do + entries + |> Enum.chunk_every(100) + |> Enum.all?(fn entries_chunk -> + Repository.has_all_object_ids?( + repository, + Enum.map(entries_chunk, fn %{object_id: id} -> id end) + ) + end) + end + + defp write_all_objects(repository, objects) + + defp write_all_objects(_repository, []), do: cover(:ok) + + defp write_all_objects(repository, [object | tail]) do + case Repository.put_loose_object(repository, object) do + :ok -> write_all_objects(repository, tail) + {:error, :object_exists} -> write_all_objects(repository, tail) + {:error, reason} -> cover {:error, reason} + end + end + @impl true def handle_call(:valid_working_tree?, _from, state), do: {:reply, :valid_working_tree, state} @@ -252,6 +371,9 @@ defmodule Xgit.Repository.WorkingTree do def handle_call({:update_dir_cache, add, remove}, _from, state), do: handle_update_dir_cache(add, remove, state) + def handle_call({:write_tree, missing_ok?, prefix}, _from, state), + do: handle_write_tree(missing_ok?, prefix, state) + def handle_call(message, _from, state) do Logger.warn("WorkingTree received unrecognized call #{inspect(message)}") {:reply, {:error, :unknown_message}, state} diff --git a/test/xgit/repository/working_tree/write_tree_test.exs b/test/xgit/repository/working_tree/write_tree_test.exs new file mode 100644 index 0000000..8a9e4ff --- /dev/null +++ b/test/xgit/repository/working_tree/write_tree_test.exs @@ -0,0 +1,952 @@ +defmodule Xgit.Repository.WorkingTree.WriteTreeTest do + use Xgit.GitInitTestCase, async: true + + alias Xgit.Core.DirCache.Entry + alias Xgit.GitInitTestCase + alias Xgit.Plumbing.HashObject + alias Xgit.Repository + alias Xgit.Repository.OnDisk + alias Xgit.Repository.WorkingTree + + import FolderDiff + + describe "write_tree/2" do + test "happy path: empty dir cache" do + assert_same_output(fn _git_dir -> nil end, fn _xgit_repo -> nil end) + end + + test "happy path: one root-level entry in dir cache" do + assert_same_output( + fn git_dir -> + {_output, 0} = + System.cmd( + "git", + [ + "update-index", + "--add", + "--cacheinfo", + "100644", + "7919e8900c3af541535472aebd56d44222b7b3a3", + "hello.txt" + ], + cd: git_dir + ) + end, + fn xgit_repo -> + working_tree = Repository.default_working_tree(xgit_repo) + + assert :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100644, + mtime: 0, + mtime_ns: 0, + name: 'hello.txt', + object_id: "7919e8900c3af541535472aebd56d44222b7b3a3", + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + end, + missing_ok?: true + ) + end + + test "can ignore existing tree objects" do + {:ok, ref: _ref, xgit: xgit} = GitInitTestCase.setup_git_repo() + + :ok = OnDisk.create(xgit) + {:ok, repo} = OnDisk.start_link(work_dir: xgit) + + working_tree = Repository.default_working_tree(repo) + + assert :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100644, + mtime: 0, + mtime_ns: 0, + name: 'hello.txt', + object_id: "7919e8900c3af541535472aebd56d44222b7b3a3", + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + + assert {:ok, xgit_object_id} = WorkingTree.write_tree(working_tree, missing_ok?: true) + + assert {:ok, ^xgit_object_id} = WorkingTree.write_tree(working_tree, missing_ok?: true) + end + + test "happy path: one blob nested one level" do + assert_same_output( + fn git_dir -> + {_output, 0} = + System.cmd( + "git", + [ + "update-index", + "--add", + "--cacheinfo", + "100644", + "7fa62716fc68733db4c769fe678295cf4cf5b336", + "a/b" + ], + cd: git_dir + ) + end, + fn xgit_repo -> + working_tree = Repository.default_working_tree(xgit_repo) + + assert :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100644, + mtime: 0, + mtime_ns: 0, + name: 'a/b', + object_id: "7fa62716fc68733db4c769fe678295cf4cf5b336", + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + end, + missing_ok?: true + ) + end + + test "happy path: deeply nested dir cache" do + assert_same_output( + fn git_dir -> + {_output, 0} = + System.cmd( + "git", + [ + "update-index", + "--add", + "--cacheinfo", + "100644", + "7fa62716fc68733db4c769fe678295cf4cf5b336", + "a/a/b" + ], + cd: git_dir + ) + + {_output, 0} = + System.cmd( + "git", + [ + "update-index", + "--add", + "--cacheinfo", + "100644", + "0f717230e297de82d0f8d761143dc1e1145c6bd5", + "a/b/c" + ], + cd: git_dir + ) + + {_output, 0} = + System.cmd( + "git", + [ + "update-index", + "--add", + "--cacheinfo", + "100644", + "ff287368514462578ba6406d366113953539cbf1", + "a/b/d" + ], + cd: git_dir + ) + + {_output, 0} = + System.cmd( + "git", + [ + "update-index", + "--add", + "--cacheinfo", + "100644", + "de588889c4d62aaf3ef3bd90be38fa239be2f5d1", + "a/c/x" + ], + cd: git_dir + ) + + {_output, 0} = + System.cmd( + "git", + [ + "update-index", + "--add", + "--cacheinfo", + "100755", + "7919e8900c3af541535472aebd56d44222b7b3a3", + "other.txt" + ], + cd: git_dir + ) + end, + fn xgit_repo -> + working_tree = Repository.default_working_tree(xgit_repo) + + assert :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100644, + mtime: 0, + mtime_ns: 0, + name: 'a/a/b', + object_id: "7fa62716fc68733db4c769fe678295cf4cf5b336", + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + + assert :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100644, + mtime: 0, + mtime_ns: 0, + name: 'a/b/c', + object_id: "0f717230e297de82d0f8d761143dc1e1145c6bd5", + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + + assert :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100644, + mtime: 0, + mtime_ns: 0, + name: 'a/b/d', + object_id: "ff287368514462578ba6406d366113953539cbf1", + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + + assert :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100644, + mtime: 0, + mtime_ns: 0, + name: 'a/c/x', + object_id: "de588889c4d62aaf3ef3bd90be38fa239be2f5d1", + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + + assert :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100755, + mtime: 0, + mtime_ns: 0, + name: 'other.txt', + object_id: "7919e8900c3af541535472aebd56d44222b7b3a3", + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + end, + missing_ok?: true + ) + end + + test "honors prefix" do + assert_same_output( + fn git_dir -> + {_output, 0} = + System.cmd( + "git", + [ + "update-index", + "--add", + "--cacheinfo", + "100644", + "7fa62716fc68733db4c769fe678295cf4cf5b336", + "a/a/b" + ], + cd: git_dir + ) + + {_output, 0} = + System.cmd( + "git", + [ + "update-index", + "--add", + "--cacheinfo", + "100644", + "0f717230e297de82d0f8d761143dc1e1145c6bd5", + "a/b/c" + ], + cd: git_dir + ) + + {_output, 0} = + System.cmd( + "git", + [ + "update-index", + "--add", + "--cacheinfo", + "100644", + "ff287368514462578ba6406d366113953539cbf1", + "a/b/d" + ], + cd: git_dir + ) + + {_output, 0} = + System.cmd( + "git", + [ + "update-index", + "--add", + "--cacheinfo", + "100644", + "de588889c4d62aaf3ef3bd90be38fa239be2f5d1", + "a/c/x" + ], + cd: git_dir + ) + + {_output, 0} = + System.cmd( + "git", + [ + "update-index", + "--add", + "--cacheinfo", + "100755", + "7919e8900c3af541535472aebd56d44222b7b3a3", + "other.txt" + ], + cd: git_dir + ) + end, + fn xgit_repo -> + working_tree = Repository.default_working_tree(xgit_repo) + + assert :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100644, + mtime: 0, + mtime_ns: 0, + name: 'a/a/b', + object_id: "7fa62716fc68733db4c769fe678295cf4cf5b336", + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + + assert :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100644, + mtime: 0, + mtime_ns: 0, + name: 'a/b/c', + object_id: "0f717230e297de82d0f8d761143dc1e1145c6bd5", + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + + assert :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100644, + mtime: 0, + mtime_ns: 0, + name: 'a/b/d', + object_id: "ff287368514462578ba6406d366113953539cbf1", + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + + assert :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100644, + mtime: 0, + mtime_ns: 0, + name: 'a/c/x', + object_id: "de588889c4d62aaf3ef3bd90be38fa239be2f5d1", + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + + assert :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100755, + mtime: 0, + mtime_ns: 0, + name: 'other.txt', + object_id: "7919e8900c3af541535472aebd56d44222b7b3a3", + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + end, + missing_ok?: true, + prefix: 'a/b' + ) + end + + test "missing_ok?: false happy path" do + Temp.track!() + path = Temp.path!() + File.write!(path, "test content\n") + + assert_same_output( + fn git_dir -> + {output, 0} = System.cmd("git", ["hash-object", "-w", path], cd: git_dir) + object_id = String.trim(output) + + {_output, 0} = + System.cmd( + "git", + ["update-index", "--add", "--cacheinfo", "100644", object_id, "a/b"], + cd: git_dir + ) + end, + fn xgit_repo -> + # Ideally, this should not reach up-level to plumbing, but I'm cheating here today. + {:ok, object_id} = HashObject.run("test content\n", repo: xgit_repo, write?: true) + + working_tree = Repository.default_working_tree(xgit_repo) + + assert :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100644, + mtime: 0, + mtime_ns: 0, + name: 'a/b', + object_id: object_id, + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + end + ) + end + + test "missing_ok? error" do + {:ok, ref: _ref, xgit: xgit} = GitInitTestCase.setup_git_repo() + + :ok = OnDisk.create(xgit) + {:ok, repo} = OnDisk.start_link(work_dir: xgit) + + :ok + + working_tree = Repository.default_working_tree(repo) + + assert :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100644, + mtime: 0, + mtime_ns: 0, + name: 'hello.txt', + object_id: "7919e8900c3af541535472aebd56d44222b7b3a3", + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + + assert {:error, :objects_missing} = WorkingTree.write_tree(working_tree) + end + + test "prefix doesn't exist" do + {:ok, ref: _ref, xgit: xgit} = GitInitTestCase.setup_git_repo() + + :ok = OnDisk.create(xgit) + {:ok, repo} = OnDisk.start_link(work_dir: xgit) + + working_tree = Repository.default_working_tree(repo) + + :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100644, + mtime: 0, + mtime_ns: 0, + name: 'a/a/b', + object_id: "7fa62716fc68733db4c769fe678295cf4cf5b336", + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + + :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100644, + mtime: 0, + mtime_ns: 0, + name: 'a/b/c', + object_id: "0f717230e297de82d0f8d761143dc1e1145c6bd5", + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + + :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100644, + mtime: 0, + mtime_ns: 0, + name: 'a/b/d', + object_id: "ff287368514462578ba6406d366113953539cbf1", + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + + :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100644, + mtime: 0, + mtime_ns: 0, + name: 'a/c/x', + object_id: "de588889c4d62aaf3ef3bd90be38fa239be2f5d1", + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + + :ok = + WorkingTree.update_dir_cache( + working_tree, + [ + %Entry{ + assume_valid?: false, + ctime: 0, + ctime_ns: 0, + dev: 0, + extended?: false, + gid: 0, + ino: 0, + intent_to_add?: false, + mode: 0o100755, + mtime: 0, + mtime_ns: 0, + name: 'other.txt', + object_id: "7919e8900c3af541535472aebd56d44222b7b3a3", + size: 0, + skip_worktree?: false, + stage: 0, + uid: 0 + } + ], + [] + ) + + assert {:error, :prefix_not_found} = + WorkingTree.write_tree(working_tree, missing_ok?: true, prefix: 'no/such/prefix') + end + + test "error: invalid dir cache" do + {:ok, ref: _ref, xgit: xgit} = GitInitTestCase.setup_git_repo() + + :ok = OnDisk.create(xgit) + + index_path = Path.join([xgit, ".git", "index"]) + File.write!(index_path, "not a valid index file") + + {:ok, repo} = OnDisk.start_link(work_dir: xgit) + + working_tree = Repository.default_working_tree(repo) + + assert {:error, :invalid_format} = WorkingTree.write_tree(working_tree, missing_ok?: true) + end + + test "error: :missing_ok? invalid" do + {:ok, ref: _ref, xgit: xgit} = GitInitTestCase.setup_git_repo() + + :ok = OnDisk.create(xgit) + {:ok, repo} = OnDisk.start_link(work_dir: xgit) + + working_tree = Repository.default_working_tree(repo) + + assert_raise ArgumentError, + ~s(Xgit.Repository.WorkingTree.write_tree/2: missing_ok? "sure" is invalid), + fn -> + WorkingTree.write_tree(working_tree, missing_ok?: "sure") + end + end + + @valid_entry %Entry{ + name: 'hello.txt', + stage: 0, + object_id: "7919e8900c3af541535472aebd56d44222b7b3a3", + mode: 0o100644, + size: 42, + ctime: 1_565_612_933, + ctime_ns: 0, + mtime: 1_565_612_941, + mtime_ns: 0, + dev: 0, + ino: 0, + uid: 0, + gid: 0, + assume_valid?: true, + extended?: false, + skip_worktree?: false, + intent_to_add?: false + } + + test "error: incomplete merge" do + {:ok, ref: _ref, xgit: xgit} = GitInitTestCase.setup_git_repo() + + :ok = OnDisk.create(xgit) + {:ok, repo} = OnDisk.start_link(work_dir: xgit) + + working_tree = Repository.default_working_tree(repo) + + :ok = + WorkingTree.update_dir_cache( + working_tree, + [@valid_entry, Map.put(@valid_entry, :stage, 1)], + [] + ) + + assert {:error, :incomplete_merge} = WorkingTree.write_tree(working_tree, missing_ok?: true) + end + + test "error: :prefix invalid" do + {:ok, ref: _ref, xgit: xgit} = GitInitTestCase.setup_git_repo() + + :ok = OnDisk.create(xgit) + {:ok, repo} = OnDisk.start_link(work_dir: xgit) + + working_tree = Repository.default_working_tree(repo) + + assert_raise ArgumentError, + ~s[Xgit.Repository.WorkingTree.write_tree/2: prefix "a/b/c" is invalid (should be a charlist, not a String)], + fn -> + WorkingTree.write_tree(working_tree, prefix: "a/b/c") + end + end + + defp assert_same_output(git_ref_fn, xgit_fn, opts \\ []) do + {:ok, ref: ref, xgit: xgit} = GitInitTestCase.setup_git_repo() + + missing_ok? = Keyword.get(opts, :missing_ok?, false) + prefix = Keyword.get(opts, :prefix, []) + + git_ref_fn.(ref) + + git_opts = + ["write-tree"] + |> maybe_add_missing_ok?(missing_ok?) + |> maybe_add_prefix(prefix) + + {output, 0} = System.cmd("git", git_opts, cd: ref) + git_ref_object_id = String.trim(output) + + :ok = OnDisk.create(xgit) + {:ok, repo} = OnDisk.start_link(work_dir: xgit) + + xgit_fn.(repo) + + working_tree = Repository.default_working_tree(repo) + assert {:ok, xgit_object_id} = WorkingTree.write_tree(working_tree, opts) + + assert_folders_are_equal( + Path.join([ref, ".git", "objects"]), + Path.join([xgit, ".git", "objects"]) + ) + + assert git_ref_object_id == xgit_object_id + end + + defp maybe_add_missing_ok?(git_opts, false), do: git_opts + defp maybe_add_missing_ok?(git_opts, true), do: git_opts ++ ["--missing-ok"] + + defp maybe_add_prefix(git_opts, []), do: git_opts + defp maybe_add_prefix(git_opts, prefix), do: git_opts ++ ["--prefix=#{prefix}"] + end +end