split-by-codeowners 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -27714,6 +27714,35 @@ exports.parseCodeowners = parseCodeowners;
27714
27714
  exports.ownersForFile = ownersForFile;
27715
27715
  const node_fs_1 = __importDefault(__nccwpck_require__(3024));
27716
27716
  const minimatch_1 = __nccwpck_require__(6507);
27717
+ function splitPatternOwners(line) {
27718
+ // CODEOWNERS lines are: <pattern><whitespace><owner>...
27719
+ // Patterns may contain spaces if escaped as `\ `.
27720
+ // We parse by finding the first whitespace not escaped by a backslash.
27721
+ let i = 0;
27722
+ let escaped = false;
27723
+ for (; i < line.length; i++) {
27724
+ const ch = line[i];
27725
+ if (escaped) {
27726
+ escaped = false;
27727
+ continue;
27728
+ }
27729
+ if (ch === "\\") {
27730
+ escaped = true;
27731
+ continue;
27732
+ }
27733
+ if (/\s/.test(ch))
27734
+ break;
27735
+ }
27736
+ const rawPattern = line.slice(0, i).trim();
27737
+ const rest = line.slice(i).trim();
27738
+ if (!rawPattern || !rest)
27739
+ return null;
27740
+ const pattern = rawPattern.replaceAll("\\ ", " ");
27741
+ const owners = rest.split(/\s+/).filter(Boolean);
27742
+ if (!owners.length)
27743
+ return null;
27744
+ return { pattern, owners };
27745
+ }
27717
27746
  function parseCodeowners(path) {
27718
27747
  const text = node_fs_1.default.readFileSync(path, "utf8");
27719
27748
  const rules = [];
@@ -27721,18 +27750,60 @@ function parseCodeowners(path) {
27721
27750
  const line = raw.trim();
27722
27751
  if (!line || line.startsWith("#"))
27723
27752
  continue;
27724
- const parts = line.split(/\s+/).filter(Boolean);
27725
- if (parts.length < 2)
27753
+ const parsed = splitPatternOwners(line);
27754
+ if (!parsed)
27726
27755
  continue;
27727
- rules.push({ pattern: parts[0], owners: parts.slice(1) });
27756
+ rules.push({ pattern: parsed.pattern, owners: parsed.owners });
27728
27757
  }
27729
27758
  return rules;
27730
27759
  }
27760
+ function hasGlobMagic(p) {
27761
+ // Good-enough heuristic for gitignore-style globs.
27762
+ return /[*?\[]/.test(p);
27763
+ }
27731
27764
  function matches(file, pattern) {
27732
- const f = file.replace(/\\/g, "/");
27733
- // crude but works for most repos:
27734
- const pat = pattern.startsWith("/") ? pattern.slice(1) : pattern;
27735
- return (0, minimatch_1.minimatch)(f, pat, { dot: true, matchBase: !pattern.startsWith("/") });
27765
+ // GitHub CODEOWNERS patterns are gitignore-like:
27766
+ // - Leading `/` anchors to repo root
27767
+ // - Pattern without `/` matches a basename anywhere
27768
+ // - A directory pattern matches everything under it (e.g. `/frontend/admin` should match `/frontend/admin/**`)
27769
+ const f = file.replace(/\\/g, "/").replace(/^\.?\//, "");
27770
+ let pat = pattern.replace(/\\/g, "/").trim();
27771
+ if (!pat)
27772
+ return false;
27773
+ const anchored = pat.startsWith("/");
27774
+ if (anchored)
27775
+ pat = pat.slice(1);
27776
+ // Directory patterns: trailing `/` means directory; also treat non-glob literal paths as directory-or-file match.
27777
+ const trailingSlash = pat.endsWith("/");
27778
+ if (trailingSlash)
27779
+ pat = pat.slice(0, -1);
27780
+ const isLiteral = !hasGlobMagic(pat);
27781
+ const hasSlash = pat.includes("/");
27782
+ // Literal path: match exact file OR directory subtree.
27783
+ if (isLiteral && (anchored || hasSlash)) {
27784
+ return f === pat || f.startsWith(pat + "/");
27785
+ }
27786
+ // Literal basename: match basename OR directory subtree anywhere.
27787
+ if (isLiteral && !hasSlash) {
27788
+ const base = f.split("/").pop() ?? f;
27789
+ if (base === pat)
27790
+ return true;
27791
+ return f.includes("/" + pat + "/");
27792
+ }
27793
+ // Glob patterns: use minimatch.
27794
+ // - If anchored: match from repo root.
27795
+ // - If not anchored and no slash: match basename anywhere.
27796
+ // - If not anchored but includes a slash: match relative to repo root (like a root CODEOWNERS / .gitignore).
27797
+ const matchBase = !anchored && !hasSlash;
27798
+ const mm = (0, minimatch_1.minimatch)(f, pat, { dot: true, matchBase });
27799
+ if (mm)
27800
+ return true;
27801
+ // If the pattern explicitly denotes a directory (trailing slash), also match directory subtree.
27802
+ if (trailingSlash) {
27803
+ // Convert `dir/` to subtree match.
27804
+ return (0, minimatch_1.minimatch)(f, pat + "/**", { dot: true, matchBase: false });
27805
+ }
27806
+ return false;
27736
27807
  }
27737
27808
  function ownersForFile(file, rules) {
27738
27809
  let hit;