Meziantou.Framework.CodeOwners
7.0.2
Prefix Reserved
dotnet add package Meziantou.Framework.CodeOwners --version 7.0.2
NuGet\Install-Package Meziantou.Framework.CodeOwners -Version 7.0.2
<PackageReference Include="Meziantou.Framework.CodeOwners" Version="7.0.2" />
<PackageVersion Include="Meziantou.Framework.CodeOwners" Version="7.0.2" />
<PackageReference Include="Meziantou.Framework.CodeOwners" />
paket add Meziantou.Framework.CodeOwners --version 7.0.2
#r "nuget: Meziantou.Framework.CodeOwners, 7.0.2"
#:package Meziantou.Framework.CodeOwners@7.0.2
#addin nuget:?package=Meziantou.Framework.CodeOwners&version=7.0.2
#tool nuget:?package=Meziantou.Framework.CodeOwners&version=7.0.2
Meziantou.Framework.CodeOwners
Meziantou.Framework.CodeOwners parses CODEOWNERS file. These files are common on GitHub and GitLab.
Parsing takes the dialect of the file, because the two hosts do not accept the same syntax: sections ([Name], ^[Name], [Name][2]) are a GitLab extension, and in the GitHub syntax a line starting with [ is a pattern, since [ opens a character class.
Each entry is one line of the file: a pattern and the owners it declares.
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);
Entries are returned in file order, and CODEOWNERS resolution is last-match-wins, so the owners of a path are those of the last entry whose pattern matches it:
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));
Parse throws a CodeOwnersParseException when the file is invalid. The exception reports the first error and where it is:
var ex = Assert.Throws<CodeOwnersParseException>(() => CodeOwnersFile.Parse("[Section\n* @user1", CodeOwnersDialect.GitLab));
Assert.Equal(CodeOwnersParseErrorKind.UnterminatedSectionHeader, ex.Error.Kind);
Assert.Equal(1, ex.Error.LineNumber);
Assert.Equal(1, ex.Error.LinePosition);
Assert.Equal("The CODEOWNERS file is invalid at line 1, position 1: the section header is not terminated by ']'", ex.Message);
Use TryParse when an invalid file should not throw. An overload reports the same error without allocating an exception:
var content = "[Section\n* @user1";
if (CodeOwnersFile.TryParse(content, CodeOwnersDialect.GitLab, out CodeOwnersFile? file, out CodeOwnersParseError error))
{
Assert.Fail("The content is not a valid GitLab CODEOWNERS file");
}
else
{
Assert.Null(file);
Assert.Equal("line 1, position 1: the section header is not terminated by ']'", error.ToString());
}
Parse and both TryParse overloads also accept a ReadOnlySpan<char>, so a file read into a buffer does not have to be turned into a string first. The parsed CodeOwnersFile never references the span: every value it exposes is copied out of it, and reported error positions are relative to the start of the span.
var buffer = new char[4096];
using var reader = new StringReader("*.md @docs-team"); // For instance, File.OpenText("CODEOWNERS")
var length = reader.ReadBlock(buffer);
CodeOwnersFile file = CodeOwnersFile.Parse(buffer.AsSpan(0, length), CodeOwnersDialect.GitHub);
Assert.Equal("*.md", file.Entries[0].Pattern);
A CodeOwnersFile only exists for a valid file: neither method hands back a partially parsed one. Entries, Owners and DefaultOwners are read-only views that cannot be cast back to a mutable list.
In the GitLab dialect an owner can also be a role, written @@developer, @@maintainer or @@owner (plural accepted). Anything else after @@ is an error, as is a @ inside a username.
CodeOwnersFile file = CodeOwnersFile.Parse("*.md @@maintainers", CodeOwnersDialect.GitLab);
Assert.Equal(CodeOwnerType.Role, file.Entries[0].Owners[0].Type);
Assert.Equal("maintainers", file.Entries[0].Owners[0].Name);
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net11.0 is compatible. |
-
net10.0
- Meziantou.Framework.ValueStringBuilder (>= 3.0.4)
-
net11.0
- Meziantou.Framework.ValueStringBuilder (>= 3.0.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 7.0.2 | 64 | 10/21/2026 |
| 7.0.1 | 6,072 | 9/13/2026 |
| 7.0.0 | 1,749 | 9/8/2026 |
| 6.0.1 | 122 | 9/6/2026 |
| 6.0.0 | 132 | 9/4/2026 |
| 5.0.2 | 207 | 9/3/2026 |
| 5.0.1 | 8,180 | 7/8/2026 |
| 5.0.0 | 580 | 7/5/2026 |
| 4.0.4 | 2,855 | 6/13/2026 |
| 4.0.3 | 1,043 | 5/23/2026 |
| 4.0.2 | 1,444 | 5/14/2026 |
| 4.0.1 | 308 | 5/11/2026 |
| 4.0.0 | 637 | 5/6/2026 |
| 3.0.18 | 1,929 | 4/25/2026 |
| 3.0.17 | 1,446 | 3/29/2026 |
| 3.0.16 | 20,789 | 3/15/2026 |
| 3.0.15 | 1,582 | 2/22/2026 |
| 3.0.14 | 3,232 | 1/18/2026 |
| 3.0.13 | 1,940 | 12/14/2025 |
| 3.0.12 | 12,678 | 11/16/2025 |