From 13122c57fe8c4214d68a74f30e61eaf1a8472d4b Mon Sep 17 00:00:00 2001 From: Eric Scouten Date: Mon, 30 Sep 2019 21:45:13 -0700 Subject: [PATCH] Implement Xgit.Core.Object.to_object/1. (#184) * Implement Xgit.Core.Object.to_object/1. * Silently de-duplicate parents list. * Raise ArgumentError if commit struct is invalid. --- lib/xgit/core/commit.ex | 45 +++++ test/xgit/core/commit_test.exs | 310 +++++++++++++++++++++++++++++++++ 2 files changed, 355 insertions(+) diff --git a/lib/xgit/core/commit.ex b/lib/xgit/core/commit.ex index 6ac8189..374de92 100644 --- a/lib/xgit/core/commit.ex +++ b/lib/xgit/core/commit.ex @@ -2,6 +2,7 @@ defmodule Xgit.Core.Commit do @moduledoc ~S""" Represents a git `commit` object in memory. """ + alias Xgit.Core.Object alias Xgit.Core.ObjectId alias Xgit.Core.PersonIdent @@ -51,4 +52,48 @@ defmodule Xgit.Core.Commit do end def valid?(_), do: cover(false) + + @doc ~S""" + Renders this commit structure into a corresponding `Xgit.Core.Object`. + + If duplicate parents are detected, they will be silently de-duplicated. + + If the commit structure is not valid, will raise `ArgumentError`. + """ + @spec to_object(commit :: t) :: Object.t() + def to_object(commit) + + def to_object( + %__MODULE__{ + tree: tree, + parents: parents, + author: %PersonIdent{} = author, + committer: %PersonIdent{} = committer, + message: message + } = commit + ) do + unless valid?(commit) do + raise ArgumentError, "Xgit.Core.Commit.to_object/1: commit is not valid" + end + + rendered_parents = + parents + |> Enum.uniq() + |> Enum.flat_map(&'parent #{&1}\n') + + rendered_commit = + 'tree #{tree}\n' ++ + rendered_parents ++ + 'author #{PersonIdent.to_external_string(author)}\n' ++ + 'committer #{PersonIdent.to_external_string(committer)}\n' ++ + '\n' ++ + message + + %Object{ + type: :commit, + content: rendered_commit, + size: Enum.count(rendered_commit), + id: ObjectId.calculate_id(rendered_commit, :commit) + } + end end diff --git a/test/xgit/core/commit_test.exs b/test/xgit/core/commit_test.exs index 0cc14bc..1db0f09 100644 --- a/test/xgit/core/commit_test.exs +++ b/test/xgit/core/commit_test.exs @@ -2,7 +2,20 @@ defmodule Xgit.Core.CommitTest do use ExUnit.Case, async: true alias Xgit.Core.Commit + alias Xgit.Core.Object alias Xgit.Core.PersonIdent + alias Xgit.GitInitTestCase + alias Xgit.Repository + alias Xgit.Repository.OnDisk + + import FolderDiff + + @valid_pi %PersonIdent{ + name: "A. U. Thor", + email: "author@example.com", + when: 1_142_878_501_000, + tz_offset: 150 + } @invalid_pi %PersonIdent{ name: :bogus, @@ -199,4 +212,301 @@ defmodule Xgit.Core.CommitTest do |> PersonIdent.from_byte_list() end end + + describe "to_object/1" do + test "empty tree" do + assert_same_output( + fn _git_dir -> [] end, + fn tree_id, [] -> + %Commit{ + tree: tree_id, + author: @valid_pi, + committer: @valid_pi, + message: 'x\n' + } + end + ) + end + + test "tree with two entries" do + assert_same_output( + fn git_dir -> + {_output, 0} = + System.cmd( + "git", + [ + "update-index", + "--add", + "--cacheinfo", + "100644", + "7919e8900c3af541535472aebd56d44222b7b3a3", + "hello.txt" + ], + cd: git_dir + ) + + {_output, 0} = + System.cmd( + "git", + [ + "update-index", + "--add", + "--cacheinfo", + "100755", + "4a43a489f107e7ece679950f53567c648038449a", + "xyzzy.sh" + ], + cd: git_dir + ) + + [] + end, + fn tree_id, [] -> + %Commit{ + tree: tree_id, + author: @valid_pi, + committer: @valid_pi, + message: 'x\n' + } + end + ) + end + + test "tree with two entries and one parent" do + assert_same_output( + fn git_dir -> + {empty_tree_id_str, 0} = + System.cmd( + "git", + [ + "write-tree" + ], + cd: git_dir + ) + + empty_tree_id = String.trim(empty_tree_id_str) + + env = [ + {"GIT_AUTHOR_DATE", "1142878449 +0230"}, + {"GIT_COMMITTER_DATE", "1142878449 +0230"}, + {"GIT_AUTHOR_EMAIL", "author@example.com"}, + {"GIT_COMMITTER_EMAIL", "author@example.com"}, + {"GIT_AUTHOR_NAME", "A. U. Thor"}, + {"GIT_COMMITTER_NAME", "A. U. Thor"} + ] + + {empty_commit_id_str, 0} = + System.cmd( + "git", + [ + "commit-tree", + "-m", + "empty", + empty_tree_id + ], + cd: git_dir, + env: env + ) + + empty_commit_id = String.trim(empty_commit_id_str) + + {_output, 0} = + System.cmd( + "git", + [ + "update-index", + "--add", + "--cacheinfo", + "100644", + "7919e8900c3af541535472aebd56d44222b7b3a3", + "hello.txt" + ], + cd: git_dir + ) + + {_output, 0} = + System.cmd( + "git", + [ + "update-index", + "--add", + "--cacheinfo", + "100755", + "4a43a489f107e7ece679950f53567c648038449a", + "xyzzy.sh" + ], + cd: git_dir + ) + + [empty_commit_id] + end, + fn tree_id, parents -> + %Commit{ + tree: tree_id, + parents: parents, + author: @valid_pi, + committer: @valid_pi, + message: 'x\n' + } + end + ) + end + + test "deduplicates and warns on duplicate parent" do + assert_same_output( + fn git_dir -> + {empty_tree_id_str, 0} = + System.cmd( + "git", + [ + "write-tree" + ], + cd: git_dir + ) + + empty_tree_id = String.trim(empty_tree_id_str) + + env = [ + {"GIT_AUTHOR_DATE", "1142878449 +0230"}, + {"GIT_COMMITTER_DATE", "1142878449 +0230"}, + {"GIT_AUTHOR_EMAIL", "author@example.com"}, + {"GIT_COMMITTER_EMAIL", "author@example.com"}, + {"GIT_AUTHOR_NAME", "A. U. Thor"}, + {"GIT_COMMITTER_NAME", "A. U. Thor"} + ] + + {empty_commit_id_str, 0} = + System.cmd( + "git", + [ + "commit-tree", + "-m", + "empty", + empty_tree_id + ], + cd: git_dir, + env: env + ) + + empty_commit_id = String.trim(empty_commit_id_str) + + {_output, 0} = + System.cmd( + "git", + [ + "update-index", + "--add", + "--cacheinfo", + "100644", + "7919e8900c3af541535472aebd56d44222b7b3a3", + "hello.txt" + ], + cd: git_dir + ) + + {_output, 0} = + System.cmd( + "git", + [ + "update-index", + "--add", + "--cacheinfo", + "100755", + "4a43a489f107e7ece679950f53567c648038449a", + "xyzzy.sh" + ], + cd: git_dir + ) + + [empty_commit_id, empty_commit_id] + end, + fn tree_id, parents -> + %Commit{ + tree: tree_id, + parents: parents, + author: @valid_pi, + committer: @valid_pi, + message: 'x\n' + } + end + ) + end + + test "raises ArgumentError if commit is invalid" do + assert_raise ArgumentError, "Xgit.Core.Commit.to_object/1: commit is not valid", fn -> + Commit.to_object(%Commit{ + tree: "be9bfa841874ccc9f2ef7c48d0c76226f89b7189", + author: @invalid_pi, + committer: pi("<> 0 +0000"), + message: 'x' + }) + end + end + + defp assert_same_output(write_tree_fn, xgit_fn, opts \\ []) do + author_date = Keyword.get(opts, :author_date, "1142878501 +0230") + committer_date = Keyword.get(opts, :committer_date, "1142878501 +0230") + + author_name = Keyword.get(opts, :author_name, "A. U. Thor") + committer_name = Keyword.get(opts, :committer_name, "A. U. Thor") + + author_email = Keyword.get(opts, :author_email, "author@example.com") + committer_email = Keyword.get(opts, :committer_email, "author@example.com") + + message = Keyword.get(opts, :message, "x") + + env = [ + {"GIT_AUTHOR_DATE", author_date}, + {"GIT_COMMITTER_DATE", committer_date}, + {"GIT_AUTHOR_EMAIL", author_email}, + {"GIT_COMMITTER_EMAIL", committer_email}, + {"GIT_AUTHOR_NAME", author_name}, + {"GIT_COMMITTER_NAME", committer_name} + ] + + {:ok, ref: ref, xgit: xgit} = GitInitTestCase.setup_git_repo() + + ref_parents = write_tree_fn.(ref) + + {output, 0} = System.cmd("git", ["write-tree", "--missing-ok"], cd: ref) + tree_content_id = String.trim(output) + + {output, 0} = + System.cmd( + "git", + ["commit-tree", tree_content_id, "-m", message] ++ + Enum.flat_map(Enum.uniq(ref_parents), &["-p", &1]), + cd: ref, + env: env + ) + + ref_commit_id = String.trim(output) + + :ok = OnDisk.create(xgit) + {:ok, repo} = OnDisk.start_link(work_dir: xgit) + + parents = write_tree_fn.(xgit) + assert parents == ref_parents + + xgit_commit_object = + tree_content_id + |> xgit_fn.(parents) + |> Commit.to_object() + + assert Object.valid?(xgit_commit_object) + assert :ok = Object.check(xgit_commit_object) + + assert xgit_commit_object.id == ref_commit_id + + {output, 0} = System.cmd("git", ["write-tree", "--missing-ok"], cd: xgit) + assert tree_content_id == String.trim(output) + + :ok = Repository.put_loose_object(repo, xgit_commit_object) + + assert_folders_are_equal( + Path.join([ref, ".git", "objects"]), + Path.join([xgit, ".git", "objects"]) + ) + end + end end