@@ -25759,7 +25759,7 @@ function getReviewForStatusFor(codeowner, currentReviewStatus) {
2575925759
2576025760// actions/codeowners-review-analysis/src/strings.ts
2576125761var LEGEND = `Legend: ${iconFor(PullRequestReviewStateExt.Approved)} Approved | ${iconFor(PullRequestReviewStateExt.ChangesRequested)} Changes Requested | ${iconFor(PullRequestReviewStateExt.Commented)} Commented | ${iconFor(PullRequestReviewStateExt.Dismissed)} Dismissed | ${iconFor(PullRequestReviewStateExt.Pending)} Pending | ${iconFor("UNKNOWN" /* Unknown */)} Unknown`;
25762- function formatPendingReviewsMarkdown(entryMap, summaryUrl) {
25762+ function formatPendingReviewsMarkdown(entryMap, summaryUrl, minimumHittingSets ) {
2576325763 const lines = [];
2576425764 lines.push("### Codeowners Review Summary");
2576525765 lines.push("");
@@ -25782,13 +25782,18 @@ function formatPendingReviewsMarkdown(entryMap, summaryUrl) {
2578225782 `| ${patternCell} | ${overallIcon} | ${processed.files.length} |${owners.join(", ")} |`
2578325783 );
2578425784 }
25785- if (summaryUrl) {
25785+ const recommendations = getReviewerRecommendations(minimumHittingSets, 2);
25786+ if (recommendations.length > 0) {
2578625787 lines.push("");
25787- lines.push(
25788- `For more details, see the [full review summary](${summaryUrl}).`
25789- );
25788+ lines.push("### Reviewer Recommendations");
25789+ recommendations.forEach((rec) => {
25790+ lines.push(`- ${rec}`);
25791+ });
2579025792 lines.push("");
2579125793 }
25794+ lines.push("");
25795+ lines.push("---");
25796+ lines.push("");
2579225797 const {
2579325798 runId,
2579425799 repo: { owner, repo }
@@ -25798,10 +25803,26 @@ function formatPendingReviewsMarkdown(entryMap, summaryUrl) {
2579825803 `Refresh analysis with: \`gh run rerun ${runId} -R ${owner}/${repo}\``
2579925804 );
2580025805 }
25806+ if (summaryUrl) {
25807+ lines.push("");
25808+ lines.push(
25809+ `For more details, see the [full review summary](${summaryUrl}).`
25810+ );
25811+ lines.push("");
25812+ }
2580125813 return lines.join("\n");
2580225814}
25803- async function formatAllReviewsSummaryByEntry(entryMap) {
25815+ async function formatAllReviewsSummaryByEntry(entryMap, minimumHittingSets ) {
2580425816 core6.summary.addHeading("Codeowners Review Details", 2).addRaw(LEGEND).addBreak();
25817+ const recommendations = getReviewerRecommendations(minimumHittingSets, 10);
25818+ if (recommendations.length > 0) {
25819+ core6.summary.addHeading(
25820+ `Reviewer Recommendations (${recommendations.length} of ${minimumHittingSets.size})`,
25821+ 3
25822+ );
25823+ core6.summary.addList(recommendations);
25824+ core6.summary.addBreak();
25825+ }
2580525826 const sortedEntries = [...entryMap.entries()].sort(([a, _], [b, __]) => {
2580625827 return a.lineNumber - b.lineNumber;
2580725828 });
@@ -25880,6 +25901,114 @@ async function formatAllReviewsSummaryByEntry(entryMap) {
2588025901 }
2588125902 await core6.summary.addSeparator().write();
2588225903}
25904+ function getReviewerRecommendations(minimumHittingSets, limit = 3) {
25905+ if (minimumHittingSets.size === 0) {
25906+ return [];
25907+ }
25908+ const setsArray = Array.from(minimumHittingSets);
25909+ const minimumSize = setsArray[0].length;
25910+ const numberOfSetsToSuggest = Math.min(minimumSize, limit, setsArray.length);
25911+ const trimmedSets = setsArray.slice(0, numberOfSetsToSuggest);
25912+ return trimmedSets.map((set2) => `${set2.join(", ")}`);
25913+ }
25914+
25915+ // actions/codeowners-review-analysis/src/hitting-sets.ts
25916+ function calculateAllMinimumHittingSets(reviewSummary) {
25917+ const { superset, subsets } = getSupersetAndSubsets(reviewSummary);
25918+ if (superset.size === 0 || superset.size > 12 || subsets.length === 0) {
25919+ return /* @__PURE__ */ new Set();
25920+ }
25921+ for (let k = 1; k <= superset.size; k++) {
25922+ const validHittingSets = /* @__PURE__ */ new Set();
25923+ for (const combo of combinations(superset, k)) {
25924+ const candidateSet = new Set(combo);
25925+ const hitsAll = subsets.every((subset) => {
25926+ for (const elem of subset) {
25927+ if (candidateSet.has(elem)) {
25928+ return true;
25929+ }
25930+ }
25931+ return false;
25932+ });
25933+ if (hitsAll) {
25934+ validHittingSets.add(combo);
25935+ }
25936+ }
25937+ if (validHittingSets.size > 0) {
25938+ return validHittingSets;
25939+ }
25940+ }
25941+ return /* @__PURE__ */ new Set();
25942+ }
25943+ function combinations(superset, k) {
25944+ const supersetArr = Array.from(superset).sort();
25945+ const results = [];
25946+ function backtrack(start, path) {
25947+ if (path.length === k) {
25948+ results.push([...path]);
25949+ return;
25950+ }
25951+ for (let i = start; i < supersetArr.length; i++) {
25952+ path.push(supersetArr[i]);
25953+ backtrack(i + 1, path);
25954+ path.pop();
25955+ }
25956+ }
25957+ backtrack(0, []);
25958+ return results;
25959+ }
25960+ function getSupersetAndSubsets(reviewSummary) {
25961+ const allPendingOwners = /* @__PURE__ */ new Set();
25962+ const allPendingEntries = [];
25963+ const subsetsSeen = /* @__PURE__ */ new Set();
25964+ for (const [entry, processed] of reviewSummary.entries()) {
25965+ if (processed.overallStatus !== PullRequestReviewStateExt.Approved) {
25966+ entry.owners.forEach((owner) => {
25967+ allPendingOwners.add(owner);
25968+ });
25969+ if (entry.owners.length > 0) {
25970+ addIfUnique(subsetsSeen, allPendingEntries, entry.owners);
25971+ }
25972+ }
25973+ }
25974+ const minimizedSubsets = removeSupersets(allPendingEntries);
25975+ return { superset: allPendingOwners, subsets: minimizedSubsets };
25976+ }
25977+ function addIfUnique(seen, set2, item) {
25978+ const normalized = JSON.stringify([...item].sort());
25979+ if (!seen.has(normalized)) {
25980+ seen.add(normalized);
25981+ set2.push(new Set(item));
25982+ }
25983+ }
25984+ function removeSupersets(sets) {
25985+ const arrs = sets.map((s) => [...s].sort());
25986+ arrs.sort((a, b) => a.length - b.length);
25987+ const keep = [];
25988+ outer: for (const S of arrs) {
25989+ for (const T of keep) {
25990+ if (isSubset(T, S)) {
25991+ continue outer;
25992+ }
25993+ }
25994+ keep.push(S);
25995+ }
25996+ return keep.map((a) => new Set(a));
25997+ }
25998+ function isSubset(A, B) {
25999+ let i = 0, j = 0;
26000+ while (i < A.length && j < B.length) {
26001+ if (A[i] === B[j]) {
26002+ i++;
26003+ j++;
26004+ } else if (A[i] > B[j]) {
26005+ j++;
26006+ } else {
26007+ return false;
26008+ }
26009+ }
26010+ return i === A.length;
26011+ }
2588326012
2588426013// actions/codeowners-review-analysis/src/run.ts
2588526014async function run() {
@@ -25932,17 +26061,18 @@ async function run() {
2593226061 );
2593326062 core7.endGroup();
2593426063 core7.startGroup("Create CODEOWNERS Summary");
25935- const codeownersSummary = createReviewSummaryObjectV2 (
26064+ const codeownersSummary = createReviewSummaryObject (
2593626065 currentPRReviewState,
2593726066 codeOwnersEntryToFiles
2593826067 );
25939- await formatAllReviewsSummaryByEntry(codeownersSummary);
26068+ const minimumHittingSets = calculateAllMinimumHittingSets(codeownersSummary);
26069+ await formatAllReviewsSummaryByEntry(codeownersSummary, minimumHittingSets);
2594026070 const summaryUrl = await getSummaryUrl(octokit, owner, repo);
2594126071 const pendingReviewMarkdown = formatPendingReviewsMarkdown(
2594226072 codeownersSummary,
25943- summaryUrl
26073+ summaryUrl,
26074+ minimumHittingSets
2594426075 );
25945- console.log(pendingReviewMarkdown);
2594626076 if (inputs.postComment) {
2594726077 await upsertPRComment(
2594826078 octokit,
@@ -25958,7 +26088,7 @@ async function run() {
2595826088 core7.setFailed(`Action failed: ${error}`);
2595926089 }
2596026090}
25961- function createReviewSummaryObjectV2 (currentReviewStatus, codeOwnersEntryToFiles) {
26091+ function createReviewSummaryObject (currentReviewStatus, codeOwnersEntryToFiles) {
2596226092 const reviewSummary = /* @__PURE__ */ new Map();
2596326093 for (const [entry, files] of codeOwnersEntryToFiles.entries()) {
2596426094 const ownerReviewStatuses = [];
0 commit comments