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-cli/index.js CHANGED
@@ -2073,6 +2073,35 @@ exports.parseCodeowners = parseCodeowners;
2073
2073
  exports.ownersForFile = ownersForFile;
2074
2074
  const node_fs_1 = __importDefault(__nccwpck_require__(24));
2075
2075
  const minimatch_1 = __nccwpck_require__(507);
2076
+ function splitPatternOwners(line) {
2077
+ // CODEOWNERS lines are: <pattern><whitespace><owner>...
2078
+ // Patterns may contain spaces if escaped as `\ `.
2079
+ // We parse by finding the first whitespace not escaped by a backslash.
2080
+ let i = 0;
2081
+ let escaped = false;
2082
+ for (; i < line.length; i++) {
2083
+ const ch = line[i];
2084
+ if (escaped) {
2085
+ escaped = false;
2086
+ continue;
2087
+ }
2088
+ if (ch === "\\") {
2089
+ escaped = true;
2090
+ continue;
2091
+ }
2092
+ if (/\s/.test(ch))
2093
+ break;
2094
+ }
2095
+ const rawPattern = line.slice(0, i).trim();
2096
+ const rest = line.slice(i).trim();
2097
+ if (!rawPattern || !rest)
2098
+ return null;
2099
+ const pattern = rawPattern.replaceAll("\\ ", " ");
2100
+ const owners = rest.split(/\s+/).filter(Boolean);
2101
+ if (!owners.length)
2102
+ return null;
2103
+ return { pattern, owners };
2104
+ }
2076
2105
  function parseCodeowners(path) {
2077
2106
  const text = node_fs_1.default.readFileSync(path, "utf8");
2078
2107
  const rules = [];
@@ -2080,18 +2109,60 @@ function parseCodeowners(path) {
2080
2109
  const line = raw.trim();
2081
2110
  if (!line || line.startsWith("#"))
2082
2111
  continue;
2083
- const parts = line.split(/\s+/).filter(Boolean);
2084
- if (parts.length < 2)
2112
+ const parsed = splitPatternOwners(line);
2113
+ if (!parsed)
2085
2114
  continue;
2086
- rules.push({ pattern: parts[0], owners: parts.slice(1) });
2115
+ rules.push({ pattern: parsed.pattern, owners: parsed.owners });
2087
2116
  }
2088
2117
  return rules;
2089
2118
  }
2119
+ function hasGlobMagic(p) {
2120
+ // Good-enough heuristic for gitignore-style globs.
2121
+ return /[*?\[]/.test(p);
2122
+ }
2090
2123
  function matches(file, pattern) {
2091
- const f = file.replace(/\\/g, "/");
2092
- // crude but works for most repos:
2093
- const pat = pattern.startsWith("/") ? pattern.slice(1) : pattern;
2094
- return (0, minimatch_1.minimatch)(f, pat, { dot: true, matchBase: !pattern.startsWith("/") });
2124
+ // GitHub CODEOWNERS patterns are gitignore-like:
2125
+ // - Leading `/` anchors to repo root
2126
+ // - Pattern without `/` matches a basename anywhere
2127
+ // - A directory pattern matches everything under it (e.g. `/frontend/admin` should match `/frontend/admin/**`)
2128
+ const f = file.replace(/\\/g, "/").replace(/^\.?\//, "");
2129
+ let pat = pattern.replace(/\\/g, "/").trim();
2130
+ if (!pat)
2131
+ return false;
2132
+ const anchored = pat.startsWith("/");
2133
+ if (anchored)
2134
+ pat = pat.slice(1);
2135
+ // Directory patterns: trailing `/` means directory; also treat non-glob literal paths as directory-or-file match.
2136
+ const trailingSlash = pat.endsWith("/");
2137
+ if (trailingSlash)
2138
+ pat = pat.slice(0, -1);
2139
+ const isLiteral = !hasGlobMagic(pat);
2140
+ const hasSlash = pat.includes("/");
2141
+ // Literal path: match exact file OR directory subtree.
2142
+ if (isLiteral && (anchored || hasSlash)) {
2143
+ return f === pat || f.startsWith(pat + "/");
2144
+ }
2145
+ // Literal basename: match basename OR directory subtree anywhere.
2146
+ if (isLiteral && !hasSlash) {
2147
+ const base = f.split("/").pop() ?? f;
2148
+ if (base === pat)
2149
+ return true;
2150
+ return f.includes("/" + pat + "/");
2151
+ }
2152
+ // Glob patterns: use minimatch.
2153
+ // - If anchored: match from repo root.
2154
+ // - If not anchored and no slash: match basename anywhere.
2155
+ // - If not anchored but includes a slash: match relative to repo root (like a root CODEOWNERS / .gitignore).
2156
+ const matchBase = !anchored && !hasSlash;
2157
+ const mm = (0, minimatch_1.minimatch)(f, pat, { dot: true, matchBase });
2158
+ if (mm)
2159
+ return true;
2160
+ // If the pattern explicitly denotes a directory (trailing slash), also match directory subtree.
2161
+ if (trailingSlash) {
2162
+ // Convert `dir/` to subtree match.
2163
+ return (0, minimatch_1.minimatch)(f, pat + "/**", { dot: true, matchBase: false });
2164
+ }
2165
+ return false;
2095
2166
  }
2096
2167
  function ownersForFile(file, rules) {
2097
2168
  let hit;