This repository has been archived by the owner on Oct 8, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Implement Xgit.Core.Ref. * Fix doc-formatting typo. * Coverage for valid?/1 but not a Ref.
- Loading branch information
Showing
2 changed files
with
287 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
defmodule Xgit.Core.Ref do | ||
@moduledoc ~S""" | ||
A reference is a struct that describes a mutable pointer to a commit or similar object. | ||
A reference is a key-value pair where the key is a name in a specific format | ||
(see [`git check-ref-format`](https://git-scm.com/docs/git-check-ref-format)) | ||
and the value (`:target`) is either a SHA-1 hash or a reference to another reference key | ||
(i.e. `ref: (name-of-valid-ref)`). | ||
This structure contains the key-value pair and functions to validate both values. | ||
""" | ||
|
||
import Xgit.Util.ForceCoverage | ||
|
||
alias Xgit.Core.ObjectId | ||
|
||
@typedoc ~S""" | ||
This struct describes a single reference stored or about to be stored in a git | ||
repository. | ||
## Struct Members | ||
* `:name`: the name of the reference (typically `refs/heads/master` or similar) | ||
* `:target`: the object ID currently marked by this reference or a symbolic link | ||
(`ref: refs/heads/master` or similar) to another reference | ||
""" | ||
@type t :: %__MODULE__{ | ||
name: String.t(), | ||
target: ObjectId.t() | String.t() | ||
} | ||
|
||
@enforce_keys [:name, :target] | ||
defstruct [:name, :target] | ||
|
||
@doc ~S""" | ||
Return `true` if the struct describes a valid reference. | ||
## Options | ||
`allow_one_level?`: Set to `true` to disregard the rule requiring at least one `/` | ||
in name. (Similar to `--allow-onelevel` option.) | ||
`refspec?`: Set to `true` to allow a single `*` in the pattern. (Similar to | ||
`--refspec-pattern` option.) | ||
""" | ||
@spec valid?(ref :: any, allow_one_level?: boolean) :: boolean | ||
def valid?(ref, opts \\ []) | ||
|
||
def valid?(%__MODULE__{name: name, target: target}, opts) | ||
when is_binary(name) and is_binary(target) | ||
when is_list(opts) do | ||
valid_name?( | ||
name, | ||
Keyword.get(opts, :allow_one_level?, false), | ||
Keyword.get(opts, :refspec?, false) | ||
) && valid_target?(target) | ||
end | ||
|
||
def valid?(_, _opts), do: cover(false) | ||
|
||
defp valid_name?("@", _, _), do: cover(false) | ||
|
||
defp valid_name?(name, true, false) do | ||
all_components_valid?(name) && not Regex.match?(~r/[\x00-\x20\\\?\[:^\x7E\x7F]/, name) && | ||
not String.ends_with?(name, ".") && not String.contains?(name, "@{") | ||
end | ||
|
||
defp valid_name?(name, false, false) do | ||
String.contains?(name, "/") && valid_name?(name, true, false) && | ||
not String.contains?(name, "*") | ||
end | ||
|
||
defp valid_name?(name, false, true) do | ||
String.contains?(name, "/") && valid_name?(name, true, false) && at_most_one_asterisk?(name) | ||
end | ||
|
||
defp all_components_valid?(name) do | ||
name | ||
|> String.split("/") | ||
|> Enum.all?(&name_component_valid?/1) | ||
end | ||
|
||
defp name_component_valid?(component), do: not name_component_invalid?(component) | ||
|
||
defp name_component_invalid?(""), do: cover(true) | ||
|
||
defp name_component_invalid?(component) do | ||
String.starts_with?(component, ".") || | ||
String.ends_with?(component, ".lock") || | ||
String.contains?(component, "..") | ||
end | ||
|
||
@asterisk_re ~r/\*/ | ||
|
||
defp at_most_one_asterisk?(name) do | ||
@asterisk_re | ||
|> Regex.scan(name) | ||
|> Enum.count() | ||
|> Kernel.<=(1) | ||
end | ||
|
||
defp valid_target?(target), do: ObjectId.valid?(target) || valid_ref_target?(target) | ||
|
||
defp valid_ref_target?("ref: " <> target_name), | ||
do: valid_name?(target_name, false, false) && String.starts_with?(target_name, "refs/") | ||
|
||
defp valid_ref_target?(_), do: cover(false) | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
defmodule Xgit.Core.RefTest do | ||
use ExUnit.Case, async: true | ||
|
||
alias Xgit.Core.Ref | ||
|
||
defp assert_valid_name(name, opts \\ []) do | ||
assert {_, 0} = System.cmd("git", check_ref_format_args(name, opts)) | ||
assert Ref.valid?(%Ref{name: name, target: "155b7b4b7a6b798725df04a6cfcfb1aa042f0834"}, opts) | ||
|
||
if Enum.empty?(opts) && String.starts_with?(name, "refs/"), | ||
do: assert(Ref.valid?(%Ref{name: "refs/heads/master", target: "ref: #{name}"}, opts)) | ||
end | ||
|
||
defp refute_valid_name(name, opts \\ []) do | ||
assert {_, 1} = System.cmd("git", check_ref_format_args(name, opts)) | ||
refute Ref.valid?(%Ref{name: name, target: "155b7b4b7a6b798725df04a6cfcfb1aa042f0834"}, opts) | ||
|
||
if Enum.empty?(opts) && String.starts_with?(name, "refs/"), | ||
do: refute(Ref.valid?(%Ref{name: "refs/heads/master", target: "ref: #{name}"}, opts)) | ||
end | ||
|
||
defp check_ref_format_args(name, opts) do | ||
["check-ref-format"] | ||
|> maybe_add_allow_one_level(Keyword.get(opts, :allow_one_level?, false)) | ||
|> maybe_add_refspec(Keyword.get(opts, :refspec?, false)) | ||
|> Kernel.++([name]) | ||
end | ||
|
||
defp maybe_add_allow_one_level(args, false), do: args | ||
defp maybe_add_allow_one_level(args, true), do: args ++ ["--allow-onelevel"] | ||
|
||
defp maybe_add_refspec(args, false), do: args | ||
defp maybe_add_refspec(args, true), do: args ++ ["--refspec-pattern"] | ||
|
||
describe "valid?/1 name" do | ||
# From documentation for git check-ref-format | ||
# (https://git-scm.com/docs/git-check-ref-format): | ||
|
||
# "Git imposes the following rules on how references are named:" | ||
|
||
test "can include slash / for hierarchical (directory) grouping" do | ||
assert_valid_name("refs/heads") | ||
assert_valid_name("refs/heads/master") | ||
assert_valid_name("refs/heads/group/subgroup") | ||
end | ||
|
||
test "no slash-separated component can begin with a dot . or end with the sequence .lock." do | ||
refute_valid_name(".refs/heads") | ||
refute_valid_name("refs/.heads/master") | ||
refute_valid_name("refs/heads/.master") | ||
refute_valid_name("refs.lock/heads") | ||
refute_valid_name("refs/heads.lock") | ||
assert_valid_name("refs.lockx/heads") | ||
assert_valid_name("refs/heads_lock") | ||
end | ||
|
||
test "must contain at least one /" do | ||
# This enforces the presence of a category like heads/, tags/ etc. | ||
# but the actual names are not restricted. | ||
assert_valid_name("refs/heads") | ||
assert_valid_name("refs/heads/master") | ||
refute_valid_name("refs") | ||
refute_valid_name("") | ||
end | ||
|
||
test "if the --allow-onelevel option is used, this rule is waived" do | ||
assert_valid_name("refs/heads", allow_one_level?: true) | ||
assert_valid_name("refs/heads/master", allow_one_level?: true) | ||
assert_valid_name("refs", allow_one_level?: true) | ||
refute_valid_name("", allow_one_level?: true) | ||
# Empty name is still disallowed. | ||
end | ||
|
||
test "cannot have two consecutive dots .. anywhere" do | ||
assert_valid_name("refs/he.ds") | ||
refute_valid_name("refs/he..ds") | ||
refute_valid_name("refs/../blah") | ||
refute_valid_name("refs../heads") | ||
end | ||
|
||
test "cannot have ASCII control characters" do | ||
# (i.e. bytes whose values are lower than \040, or \177 DEL), space, tilde ~, caret ^, | ||
# or colon : anywhere. | ||
refute_valid_name("refs/he\u001fads") | ||
refute_valid_name("refs/he\u007fads") | ||
refute_valid_name("refs/he ads") | ||
refute_valid_name("refs/~heads") | ||
refute_valid_name("refs/^heads") | ||
refute_valid_name("refs/he:ads") | ||
end | ||
|
||
test "cannot have question-mark ?, asterisk *, or open bracket [ anywhere" do | ||
refute_valid_name("refs/he?ads") | ||
refute_valid_name("refs/heads?") | ||
refute_valid_name("refs/heads/*") | ||
refute_valid_name("refs/heads*/foo") | ||
refute_valid_name("refs/heads/[foo") | ||
refute_valid_name("refs/hea[ds/foo") | ||
end | ||
|
||
test "allows a single asterisk * with --refspec-pattern" do | ||
# See the --refspec-pattern option below for an exception to this rule. | ||
refute_valid_name("refs/heads/*") | ||
assert_valid_name("refs/heads/*", refspec?: true) | ||
refute_valid_name("refs/heads*/foo") | ||
assert_valid_name("refs/heads*/foo", refspec?: true) | ||
end | ||
|
||
test "cannot begin or end with a slash / or contain multiple consecutive slashes" do | ||
refute_valid_name("/refs/heads/foo") | ||
refute_valid_name("refs/heads/master/") | ||
refute_valid_name("refs//heads/master") | ||
end | ||
|
||
test "cannot end with a dot ." do | ||
assert_valid_name("refs./heads/master") | ||
refute_valid_name("refs/heads/master.") | ||
end | ||
|
||
test "cannot contain a sequence @{" do | ||
assert_valid_name("refs/heads/@master") | ||
assert_valid_name("refs/heads/{master") | ||
refute_valid_name("refs/heads/@{master") | ||
end | ||
|
||
test "cannot be the single character @" do | ||
assert_valid_name("refs/@/master") | ||
refute_valid_name("@") | ||
refute_valid_name("@", allow_one_level?: true) | ||
end | ||
|
||
test "cannot contain a \\" do | ||
refute_valid_name("refs\\heads/master") | ||
end | ||
end | ||
|
||
describe "valid?/1 target" do | ||
defp assert_valid_target(target) do | ||
assert Ref.valid?(%Ref{name: "refs/heads/master", target: target}) | ||
end | ||
|
||
defp refute_valid_target(target) do | ||
refute Ref.valid?(%Ref{name: "refs/heads/master", target: target}) | ||
end | ||
|
||
test "object ID" do | ||
assert_valid_target("1234567890abcdef12341234567890abcdef1234") | ||
refute_valid_target("1234567890abcdef1231234567890abcdef1234") | ||
refute_valid_target("1234567890abcdef123451234567890abcdef1234") | ||
refute_valid_target("1234567890abCdef12341234567890abcdef1234") | ||
refute_valid_target("1234567890abXdef12341234567890abcdef1234") | ||
|
||
refute_valid_target(nil) | ||
end | ||
|
||
test "ref" do | ||
assert_valid_target("ref: refs/heads/master") | ||
refute_valid_target("ref:") | ||
refute_valid_target("rex: refs/heads/master") | ||
refute_valid_target("ref: refs") | ||
end | ||
|
||
test "ref must point inside of refs/ hierarchy" do | ||
refute_valid_target("ref: refsxyz/heads/master") | ||
refute_valid_target("ref: rex/heads/master") | ||
end | ||
end | ||
|
||
test "valid?/1 not a Ref" do | ||
refute Ref.valid?("refs/heads/master") | ||
refute Ref.valid?(42) | ||
refute Ref.valid?(nil) | ||
|
||
refute Ref.valid?(%{ | ||
name: "refs/heads/master", | ||
target: "155b7b4b7a6b798725df04a6cfcfb1aa042f0834" | ||
}) | ||
end | ||
end |