Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,32 @@
using System.Linq;
using System.Reflection.Emit;

namespace CreateRuleFabricBot.Rules.PullRequestLabel
namespace CreateRuleFabricBot
{
/// <summary>
/// The entry for CODEOWNERS has the following structure:
/// # PRLabel: %Label
/// # ServiceLabel: %Label
/// path @owner @owner
/// </summary>
public class CodeOwnerEntry
{
const char LabelSeparator = '%';
const char OwnerSeparator = '@';
internal const string LabelMoniker = "PRLabel";

public CodeOwnerEntry(string entryLine, string labelsLine)
{
ParseLabels(labelsLine);
ParseOwnersAndPath(entryLine);
}

public CodeOwnerEntry()
{

}
internal const string PRLabelMoniker = "PRLabel";
internal const string ServiceLabelMoniker = "ServiceLabel";
internal const string MissingFolder = "#/<NotInRepo>/";

public string PathExpression { get; set; }

public bool ContainsWildcard { get; set; }

public List<string> Owners { get; set; } = new List<string>();

public List<string> Labels { get; set; } = new List<string>();
public List<string> PRLabels { get; set; } = new List<string>();

public List<string> ServiceLabels { get; set; } = new List<string>();

public bool IsValid
{
get
Expand All @@ -45,43 +45,68 @@ private static string[] SplitLine(string line, char splitOn)

public override string ToString()
{
return $"HasWildcard:{ContainsWildcard} Expression:{PathExpression} Owners:{string.Join(',', Owners)} Labels:{string.Join(',', Labels)}";
return $"HasWildcard:{ContainsWildcard} Expression:{PathExpression} Owners:{string.Join(',', Owners)} PRLabels:{string.Join(',', PRLabels)} ServiceLabels:{string.Join(',', ServiceLabels)}";
}

public bool ProcessLabelsOnLine(string line)
{
if (line.IndexOf(PRLabelMoniker, StringComparison.OrdinalIgnoreCase) >= 0)
{
PRLabels.AddRange(ParseLabels(line, PRLabelMoniker));
return true;
}
else if (line.IndexOf(ServiceLabelMoniker, StringComparison.OrdinalIgnoreCase) >= 0)
{
ServiceLabels.AddRange(ParseLabels(line, ServiceLabelMoniker));
return true;
}
return false;
}

public void ParseLabels(string line)
private IEnumerable<string> ParseLabels(string line, string moniker)
{
// Parse a line that looks like # PRLabel: %Label, %Label
if (line.IndexOf(LabelMoniker, StringComparison.OrdinalIgnoreCase) == -1)
if (line.IndexOf(moniker, StringComparison.OrdinalIgnoreCase) == -1)
{
return;
yield break;
}

// If we don't have a ':', nothing to do
int colonPosition = line.IndexOf(':');
if (colonPosition == -1)
{
return;
yield break;
}

line = line.Substring(colonPosition + 1).Trim();
foreach (string label in SplitLine(line, LabelSeparator).ToList())
{
if (!string.IsNullOrWhiteSpace(label))
{
Labels.Add(label.Trim());
yield return label.Trim();
}
}
}

public void ParseOwnersAndPath(string line)
{
if (string.IsNullOrEmpty(line) || line.StartsWith('#'))
if (string.IsNullOrEmpty(line) ||
(line.StartsWith('#') && !(line.IndexOf(CodeOwnerEntry.MissingFolder, StringComparison.OrdinalIgnoreCase) >= 0)))
{
return;
}

line = ParsePath(line);

//remove any comments from the line, if any.
// this is the case when we have something like @user #comment
int commentIndex = line.IndexOf("#");

if (commentIndex >= 0)
{
line = line.Substring(0, commentIndex).Trim();
}

foreach (string author in SplitLine(line, OwnerSeparator).ToList())
{
if (!string.IsNullOrWhiteSpace(author))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using CreateRuleFabricBot.Helpers;
using OutputColorizer;
using System.Collections.Generic;
using System.IO;

namespace CreateRuleFabricBot
{
public static class CodeOwnersFile
{
public static List<CodeOwnerEntry> ParseFile(string filePathOrUrl)
{
string content;
Colorizer.Write("Retrieving file content from [Yellow!{0}]... ", filePathOrUrl);
content = FileHelpers.GetFileContents(filePathOrUrl);
Colorizer.WriteLine("[Green!Done]");

return ParseContent(content);
}

public static List<CodeOwnerEntry> ParseContent(string fileContent)
{
Colorizer.Write("Parsing CODEOWNERS table... ");
List<CodeOwnerEntry> entries = new List<CodeOwnerEntry>();
string line;


// An entry ends when we get to a path (a real path or a commented dummy path)

using (StringReader sr = new StringReader(fileContent))
{
CodeOwnerEntry entry = new CodeOwnerEntry();

// we are going to read line by line until we find a line that is not a comment OR that is using the placeholder entry inside the comment.
// while we are trying to find the folder entry, we parse all comment lines to extract the labels from it.
// when we find the path or placeholder, we add the completed entry and create a new one.
while ((line = sr.ReadLine()) != null)
{
line = NormalizeLine(line);

if (string.IsNullOrWhiteSpace(line))
{
continue;
}

if (!line.StartsWith('#') || line.IndexOf(CodeOwnerEntry.MissingFolder, System.StringComparison.OrdinalIgnoreCase) >= 0)
{
// If this is not a comment line OR this is a placeholder entry

entry.ParseOwnersAndPath(line);

// only add it if it is a valid entry
if (entry.IsValid)
{
entries.Add(entry);
}

// create a new entry.
entry = new CodeOwnerEntry();
}
else if (line.StartsWith('#'))
{
// try to process the line in case there are markers that need to be extracted
entry.ProcessLabelsOnLine(line);
}

}
}
Colorizer.WriteLine("[Green!Done]");
return entries;
}

private static string NormalizeLine(string line)
{
if (string.IsNullOrEmpty(line))
{
return line;
}

// Remove tabs and trim extra whitespace
return line.Replace('\t', ' ').Trim();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;

namespace CreateRuleFabricBot.Helpers
{
public static class FileHelpers
{
public static string GetFileContents(string fileOrUri)
{
if (File.Exists(fileOrUri))
{
return File.ReadAllText(fileOrUri);
}

// try to parse it as an Uri
Uri uri = new Uri(fileOrUri, UriKind.Absolute);
if (uri.Scheme.ToLowerInvariant() != "https")
{
throw new ArgumentException("Cannot download off non-https uris");
}

// try to download it.
using (HttpClient client = new HttpClient())
{
HttpResponseMessage response = client.GetAsync(fileOrUri).ConfigureAwait(false).GetAwaiter().GetResult();
return response.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using CreateRuleFabricBot.Helpers;
using System;
using System.Collections.Generic;
using System.IO;

Expand All @@ -14,7 +15,7 @@ public static MarkdownTable Parse(string filePath)
MarkdownTable mt = new MarkdownTable();
string line;

using (StreamReader sr = new StreamReader(filePath))
using (StringReader sr = new StringReader(FileHelpers.GetFileContents(filePath)))
{
while ((line = sr.ReadLine()) != null)
{
Expand Down
1 change: 1 addition & 0 deletions tools/CreateRuleFabricBot/CreateRuleFabricBot/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using CreateRuleFabricBot.Markdown;
using CreateRuleFabricBot.Rules;
using CreateRuleFabricBot.Rules.IssueRouting;
using CreateRuleFabricBot.Rules.PullRequestLabel;
using CreateRuleFabricBot.Service;
using OutputColorizer;
using System;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Emit;

namespace CreateRuleFabricBot.Rules.IssueRouting
{
Expand All @@ -27,18 +28,19 @@ public class IssueRoutingCapability : BaseCapability

private readonly string _repo;
private readonly string _owner;
private readonly string _servicesFile;
private readonly string _codeownersFile;

private List<TriageConfig> _triageConfig = new List<TriageConfig>();

private int RouteCount { get { return _triageConfig.Count; } }

public IssueRoutingCapability(string org, string repo, string servicesFile)
public IssueRoutingCapability(string org, string repo, string codeownersFile)
{
_repo = repo;
_owner = org;
_servicesFile = servicesFile;
_codeownersFile = codeownersFile;
}

private void AddService(IEnumerable<string> labels, IEnumerable<string> mentionees)
{
var tc = new TriageConfig();
Expand All @@ -49,26 +51,26 @@ private void AddService(IEnumerable<string> labels, IEnumerable<string> mentione

public override string GetPayload()
{
Colorizer.Write("Parsing service table... ");
MarkdownTable table = MarkdownTable.Parse(_servicesFile);
Colorizer.WriteLine("[Green!done].");
List<CodeOwnerEntry> entries = CodeOwnersFile.ParseFile(_codeownersFile);

foreach (var row in table.Rows)
foreach (CodeOwnerEntry entry in entries)
{
if (!string.IsNullOrEmpty(row[2].Trim()))
// If we have labels for the specific codeowners entry, add that to the triage list
if (entry.ServiceLabels.Any())
{
// the row at position 0 is the label to use on top of 'Service Attention'
string[] labels = new string[] { "Service Attention", row[0] };

// The row at position 2 is the set of mentionees to ping on the issue.
IEnumerable<string> mentionees = row[2].Split(',').Select(x => x.Replace("@", "").Trim());
// Remove the '@' from the owners handle
IEnumerable<string> mentionees = entry.Owners.Select(x => x.Replace("@", "").Trim());

//add the service
AddService(labels, mentionees);
AddService(entry.ServiceLabels, mentionees);
}
}

Colorizer.WriteLine("Found [Yellow!{0}] service routes.", RouteCount);
foreach (TriageConfig triage in _triageConfig)
{
Colorizer.WriteLine("Labels:[Yellow!{0}], Owners:[Yellow!{1}]", string.Join(',', triage.Labels), string.Join(',', triage.Mentionee));
}

return s_template
.Replace("###repo###", GetTaskId())
Expand Down
Loading