Entries and owners

edit

Read the owners of an entry

Each owner is a CodeOwner: its Type says how it is written, and its Name is the identifier without the leading @:

CodeOwnersFile file = CodeOwnersFile.Parse("* @user1 docs@example.com", CodeOwnersDialect.GitHub);
Assert.Equal("*", file.Entries[0].Pattern);
Assert.Equal(CodeOwnerType.Username, file.Entries[0].Owners[0].Type);
Assert.Equal("user1", file.Entries[0].Owners[0].Name);
Assert.Equal(CodeOwnerType.EmailAddress, file.Entries[0].Owners[1].Type);
Assert.Equal("docs@example.com", file.Entries[0].Owners[1].Name);
CodeOwnerTypeWritten asName
Username@user or @org/teamuser, org/team
EmailAddressuser@example.comuser@example.com
Role@@developer, @@maintainer, @@owner (GitLab only)developer, maintainer, owner

The parser does not check that a user, a team or an address exists: it only checks the syntax. A token that is neither a username nor an email address makes the file invalid. See Handling errors.

Find the owners of a path

Entries are in the order of the file, and CODEOWNERS resolution is last-match-wins: a path is owned by the last entry whose pattern matches it. The library does not match patterns, so bring your own matcher (for instance the .gitignore dialect of Globbing) and search the entries from the end:

var file = CodeOwnersFile.Parse("""
    * @user1
    *.md @docs-team
    CHANGELOG.md
    """, CodeOwnersDialect.GitHub);

IReadOnlyList<CodeOwner> GetOwners(string path)
{
    CodeOwnersEntry? owningEntry = file.Entries.LastOrDefault(entry => Matches(entry.Pattern, path));
    return owningEntry?.Owners ?? [];
}

Assert.Equal("user1", Assert.Single(GetOwners("src/Program.cs")).Name);
Assert.Equal("docs-team", Assert.Single(GetOwners("README.md")).Name);

// An empty Owners list means the entry explicitly leaves the pattern unowned
Assert.Empty(GetOwners("CHANGELOG.md"));

// The library does not match patterns: this matcher only handles the patterns of this example
static bool Matches(string pattern, string path)
    => pattern == "*" || pattern == path || (pattern.StartsWith("*.", StringComparison.Ordinal) && path.EndsWith(pattern[1..], StringComparison.Ordinal));

The matcher of this example only handles the patterns it uses. A real one must follow the pattern syntax of the host.

Patterns without owners

An entry without owners has an empty Owners list: the entry explicitly leaves its pattern unowned. As resolution is last-match-wins, the paths it matches have no owners, even when a previous entry matches them too:

var file = CodeOwnersFile.Parse("""
    * @org/maintainers
    /generated/ # Nobody owns the generated files
    """, CodeOwnersDialect.GitHub);

CodeOwnersEntry generated = file.Entries[1];
Assert.Equal("/generated/", generated.Pattern);
Assert.Empty(generated.Owners);
  • A line that starts with # is a comment. A # after the pattern starts a comment too.
  • In a GitLab section that has default owners, an entry without owners gets the default owners of the section instead. See Default owners.

Escaped patterns

A backslash escapes the next character of a pattern, so that a pattern can contain a space or start with #. The Pattern property holds the pattern without the backslashes:

var file = CodeOwnersFile.Parse("""
    /docs/User\ Guide.md @docs-team
    \#notes.txt @user1
    """, CodeOwnersDialect.GitHub);

// The backslashes are removed from the pattern
Assert.Equal("/docs/User Guide.md", file.Entries[0].Pattern);
Assert.Equal("#notes.txt", file.Entries[1].Pattern);

A backslash at the end of a line does not join the next line: an entry never spans several lines.

Write entries back

ToString returns a value as written in a CODEOWNERS file of the dialect it was parsed with. The pattern is escaped again, so the text can be parsed back to the same entry:

var file = CodeOwnersFile.Parse("""
    [Docs][2] @docs-team
    /docs/User\ Guide.md @writer docs@example.com
    """, CodeOwnersDialect.GitLab);

CodeOwnersEntry entry = file.Entries[0];
Assert.Equal("/docs/User Guide.md", entry.Pattern);

// ToString writes the values back in the CODEOWNERS syntax, escaping the pattern again
Assert.Equal("""/docs/User\ Guide.md @writer docs@example.com""", entry.ToString());
Assert.Equal("@writer", entry.Owners[0].ToString());
Assert.Equal("[Docs][2] @docs-team", entry.Section?.ToString());

Compare entries

CodeOwnersEntry, CodeOwner and CodeOwnersSection have value equality: two entries are equal when they have the same pattern, the same owners in the same order, and equal sections. The == and != operators compare the same way.