diff --git a/tools/code-owners-parser/Azure.Sdk.Tools.RetrieveCodeOwners/Program.cs b/tools/code-owners-parser/Azure.Sdk.Tools.RetrieveCodeOwners/Program.cs
index 1661dda8d51..07ca20b85b9 100644
--- a/tools/code-owners-parser/Azure.Sdk.Tools.RetrieveCodeOwners/Program.cs
+++ b/tools/code-owners-parser/Azure.Sdk.Tools.RetrieveCodeOwners/Program.cs
@@ -37,6 +37,8 @@ public static class Program
/// Defaults to ".git".
/// Example usage: ".git|foo|bar"
///
+ /// Override for the default URI where the team/storage blob data resides.
+ /// File to output the owners data to, will overwrite if the file exist.
///
/// On STDOUT: The JSON representation of the matched CodeownersEntry.
/// "new CodeownersEntry()" if no path in the CODEOWNERS data matches.
@@ -48,7 +50,9 @@ public static int Main(
string codeownersFilePathOrUrl,
bool excludeNonUserAliases = false,
string? targetDir = null,
- string ignoredPathPrefixes = DefaultIgnoredPrefixes)
+ string ignoredPathPrefixes = DefaultIgnoredPrefixes,
+ string? teamStorageURI = null,
+ string? ownersDataOutputFile = null)
{
try
{
@@ -71,17 +75,29 @@ public static int Main(
targetDir!,
codeownersFilePathOrUrl,
excludeNonUserAliases,
- SplitIgnoredPathPrefixes())
+ SplitIgnoredPathPrefixes(),
+ teamStorageURI)
: GetCodeownersForSimplePath(
targetPath,
codeownersFilePathOrUrl,
- excludeNonUserAliases);
+ excludeNonUserAliases,
+ teamStorageURI);
string codeownersJson = JsonSerializer.Serialize(
codeownersData,
new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(codeownersJson);
+
+ // If the output data file is specified, write the json to that.
+ if (!string.IsNullOrEmpty(ownersDataOutputFile))
+ {
+ // False in the ctor is to overwrite, not append
+ using (StreamWriter outputFile = new StreamWriter(ownersDataOutputFile, false))
+ {
+ outputFile.WriteLine(codeownersJson);
+ }
+ }
return 0;
string[] SplitIgnoredPathPrefixes()
@@ -101,7 +117,8 @@ private static Dictionary GetCodeownersForGlobPath(
string targetDir,
string codeownersFilePathOrUrl,
bool excludeNonUserAliases,
- string[]? ignoredPathPrefixes = null)
+ string[]? ignoredPathPrefixes = null,
+ string? teamStorageURI=null)
{
ignoredPathPrefixes ??= Array.Empty();
@@ -110,7 +127,8 @@ private static Dictionary GetCodeownersForGlobPath(
targetPath,
targetDir,
codeownersFilePathOrUrl,
- ignoredPathPrefixes);
+ ignoredPathPrefixes,
+ teamStorageURI);
if (excludeNonUserAliases)
codeownersEntries.Values.ToList().ForEach(entry => entry.ExcludeNonUserAliases());
@@ -121,12 +139,14 @@ private static Dictionary GetCodeownersForGlobPath(
private static CodeownersEntry GetCodeownersForSimplePath(
string targetPath,
string codeownersFilePathOrUrl,
- bool excludeNonUserAliases)
+ bool excludeNonUserAliases,
+ string? teamStorageURI = null)
{
CodeownersEntry codeownersEntry =
CodeownersFile.GetMatchingCodeownersEntry(
targetPath,
- codeownersFilePathOrUrl);
+ codeownersFilePathOrUrl,
+ teamStorageURI);
if (excludeNonUserAliases)
codeownersEntry.ExcludeNonUserAliases();
diff --git a/tools/code-owners-parser/CodeOwnersParser/CodeownersEntry.cs b/tools/code-owners-parser/CodeOwnersParser/CodeownersEntry.cs
index 72f1d9fbf83..782adfd7e00 100644
--- a/tools/code-owners-parser/CodeOwnersParser/CodeownersEntry.cs
+++ b/tools/code-owners-parser/CodeOwnersParser/CodeownersEntry.cs
@@ -1,7 +1,10 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
namespace Azure.Sdk.Tools.CodeOwnersParser
{
@@ -45,7 +48,7 @@ public CodeownersEntry(string pathExpression, List owners)
}
private static string[] SplitLine(string line, char splitOn)
- => line.Split(new char[] { splitOn }, StringSplitOptions.RemoveEmptyEntries);
+ => line.Split(new char[] { splitOn }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
public override string ToString()
=> $"HasWildcard:{ContainsWildcard} Expression:{PathExpression} " +
@@ -85,14 +88,11 @@ private static IEnumerable ParseLabels(string line, string moniker)
line = line[(colonPosition + 1)..].Trim();
foreach (string label in SplitLine(line, LabelSeparator).ToList())
{
- if (!string.IsNullOrWhiteSpace(label))
- {
- yield return label.Trim();
- }
+ yield return label;
}
}
- public void ParseOwnersAndPath(string line)
+ public void ParseOwnersAndPath(string line, TeamUserHolder teamUserHolder)
{
if (
string.IsNullOrEmpty(line)
@@ -106,10 +106,42 @@ public void ParseOwnersAndPath(string line)
line = ParsePath(line);
line = RemoveCommentIfAny(line);
- foreach (string author in SplitLine(line, OwnerSeparator).ToList())
+ // If the line doesn't contain the OwnerSeparator AKA no owners, then the foreach loop below
+ // won't work. For example, the following line would end up causing "/sdk/communication" to
+ // be added as an owner when one is not listed
+ // /sdk/communication/
+ if (line.Contains(OwnerSeparator))
+ {
+ foreach (string author in SplitLine(line, OwnerSeparator).ToList())
+ {
+ // If the author is a team, get the user list and add that to the Owners
+ if (!IsGitHubUserAlias(author))
+ {
+ var teamUsers = teamUserHolder.GetUsersForTeam(author);
+ // If the team is found in team user data, add the list of users to
+ // the owners and ensure the end result is a distinct list
+ if (teamUsers.Count > 0)
+ {
+ // The union of the two lists will ensure the result a distinct list
+ Owners = Owners.Union(teamUsers).ToList();
+ }
+ // Else, the team user data did not contain an entry or there were no user
+ // for the team. In that case, just add the team to the list of authors
+ else
+ {
+ Owners.Add(author);
+ }
+ }
+ // If the entry isn't a team, then just add it
+ else
+ {
+ Owners.Add(author);
+ }
+ }
+ }
+ else
{
- if (!string.IsNullOrWhiteSpace(author))
- Owners.Add(author.Trim());
+ Console.WriteLine($"Warning: CODEOWNERS line '{line}' does not have an owner entry.");
}
}
diff --git a/tools/code-owners-parser/CodeOwnersParser/CodeownersFile.cs b/tools/code-owners-parser/CodeOwnersParser/CodeownersFile.cs
index 83f1dc376ee..110f001751a 100644
--- a/tools/code-owners-parser/CodeOwnersParser/CodeownersFile.cs
+++ b/tools/code-owners-parser/CodeOwnersParser/CodeownersFile.cs
@@ -3,20 +3,24 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
+using System.Text.Json;
namespace Azure.Sdk.Tools.CodeOwnersParser
{
public static class CodeownersFile
{
+
public static List GetCodeownersEntriesFromFileOrUrl(
- string codeownersFilePathOrUrl)
+ string codeownersFilePathOrUrl,
+ string? teamStorageURI = null)
{
string content = FileHelpers.GetFileOrUrlContents(codeownersFilePathOrUrl);
- return GetCodeownersEntries(content);
+ return GetCodeownersEntries(content, teamStorageURI);
}
- public static List GetCodeownersEntries(string codeownersContent)
+ public static List GetCodeownersEntries(string codeownersContent, string? teamStorageURI = null)
{
+ TeamUserHolder teamUserHolder = new TeamUserHolder(teamStorageURI);
List entries = new List();
// We are going to read line by line until we find a line that is not a comment
@@ -28,7 +32,7 @@ public static List GetCodeownersEntries(string codeownersConten
using StringReader sr = new StringReader(codeownersContent);
while (sr.ReadLine() is { } line)
{
- entry = ProcessCodeownersLine(line, entry, entries);
+ entry = ProcessCodeownersLine(line, entry, entries, teamUserHolder);
}
return entries;
@@ -36,9 +40,10 @@ public static List GetCodeownersEntries(string codeownersConten
public static CodeownersEntry GetMatchingCodeownersEntry(
string targetPath,
- string codeownersFilePathOrUrl)
+ string codeownersFilePathOrUrl,
+ string? teamStorageURI = null)
{
- var codeownersEntries = GetCodeownersEntriesFromFileOrUrl(codeownersFilePathOrUrl);
+ var codeownersEntries = GetCodeownersEntriesFromFileOrUrl(codeownersFilePathOrUrl, teamStorageURI);
return GetMatchingCodeownersEntry(targetPath, codeownersEntries);
}
@@ -46,11 +51,12 @@ public static Dictionary GetMatchingCodeownersEntries(
GlobFilePath targetPath,
string targetDir,
string codeownersFilePathOrUrl,
- string[]? ignoredPathPrefixes = null)
+ string[]? ignoredPathPrefixes = null,
+ string? teamStorageURI = null)
{
ignoredPathPrefixes ??= Array.Empty();
- var codeownersEntries = GetCodeownersEntriesFromFileOrUrl(codeownersFilePathOrUrl);
+ var codeownersEntries = GetCodeownersEntriesFromFileOrUrl(codeownersFilePathOrUrl, teamStorageURI);
Dictionary codeownersEntriesByPath = targetPath
.ResolveGlob(targetDir, ignoredPathPrefixes)
@@ -74,7 +80,8 @@ public static CodeownersEntry GetMatchingCodeownersEntry(
private static CodeownersEntry ProcessCodeownersLine(
string line,
CodeownersEntry entry,
- List entries)
+ List entries,
+ TeamUserHolder teamUserHolder)
{
line = NormalizeLine(line);
@@ -85,7 +92,7 @@ private static CodeownersEntry ProcessCodeownersLine(
if (!IsCommentLine(line) || (IsCommentLine(line) && IsPlaceholderEntry(line)))
{
- entry.ParseOwnersAndPath(line);
+ entry.ParseOwnersAndPath(line, teamUserHolder);
if (entry.IsValid)
entries.Add(entry);
diff --git a/tools/code-owners-parser/CodeOwnersParser/DefaultStorageConstants.cs b/tools/code-owners-parser/CodeOwnersParser/DefaultStorageConstants.cs
new file mode 100644
index 00000000000..dfc25d9b957
--- /dev/null
+++ b/tools/code-owners-parser/CodeOwnersParser/DefaultStorageConstants.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Azure.Sdk.Tools.CodeOwnersParser
+{
+ public class DefaultStorageConstants
+ {
+ public const string DefaultStorageURI = "https://azuresdkartifacts.blob.core.windows.net/azure-sdk-write-teams/azure-sdk-write-teams-blob";
+ }
+}
diff --git a/tools/code-owners-parser/CodeOwnersParser/TeamUserHolder.cs b/tools/code-owners-parser/CodeOwnersParser/TeamUserHolder.cs
new file mode 100644
index 00000000000..4a3a7c4ba60
--- /dev/null
+++ b/tools/code-owners-parser/CodeOwnersParser/TeamUserHolder.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+
+namespace Azure.Sdk.Tools.CodeOwnersParser
+{
+ public class TeamUserHolder
+ {
+ private string TeamUserStorageURI { get; set; } = DefaultStorageConstants.DefaultStorageURI;
+ private Dictionary>? _teamUserDict = null;
+
+ public Dictionary> TeamUserDict
+ {
+ get
+ {
+ if (_teamUserDict == null)
+ {
+ _teamUserDict = GetTeamUserData();
+ }
+ return _teamUserDict;
+ }
+ set
+ {
+ _teamUserDict = value;
+ }
+ }
+
+ public TeamUserHolder(string? teamUserStorageURI)
+ {
+ if (!string.IsNullOrWhiteSpace(teamUserStorageURI))
+ {
+ TeamUserStorageURI = teamUserStorageURI;
+ }
+ }
+
+ private Dictionary> GetTeamUserData()
+ {
+ if (null == _teamUserDict)
+ {
+ string rawJson = FileHelpers.GetFileOrUrlContents(TeamUserStorageURI);
+ var list = JsonSerializer.Deserialize>>>(rawJson);
+ if (null != list)
+ {
+ return list.ToDictionary((keyItem) => keyItem.Key, (valueItem) => valueItem.Value);
+ }
+ Console.WriteLine($"Error! Unable to deserialize json team/user data from {TeamUserStorageURI}. rawJson={rawJson}");
+ return new Dictionary>();
+ }
+ return _teamUserDict;
+ }
+
+ public List GetUsersForTeam(string teamName)
+ {
+ // The teamName in the codeowners file should be in the form /.
+ // The dictionary's team names do not contain the org so the org needs to
+ // be stripped off. Handle the case where the teamName passed in does and
+ // does not being with @org/
+ string teamWithoutOrg = teamName.Trim();
+ if (teamWithoutOrg.Contains('/'))
+ {
+ teamWithoutOrg = teamWithoutOrg.Split("/")[1];
+ }
+ if (TeamUserDict != null)
+ {
+ if (TeamUserDict.ContainsKey(teamWithoutOrg))
+ {
+ Console.WriteLine($"Found team entry for {teamWithoutOrg}");
+ return TeamUserDict[teamWithoutOrg];
+ }
+ Console.WriteLine($"Warning: TeamUserDictionary did not contain a team entry for {teamWithoutOrg}");
+ }
+ return new List();
+ }
+ }
+}