codeowners-git 1.8.0 → 2.0.0

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.
Files changed (3) hide show
  1. package/README.md +182 -41
  2. package/dist/cli.js +702 -494
  3. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -16080,20 +16080,6 @@ var import_cli_table3 = __toESM(require_table(), 1);
16080
16080
  var DEFAULT_COLUMN_WIDTH = 50;
16081
16081
  var MAX_LINE_LENGTH = 80;
16082
16082
  var MAX_PATH_LENGTH = 60;
16083
- var logFileList = (files, owner) => {
16084
- if (files.length === 0) {
16085
- log.info("No matching files found");
16086
- return;
16087
- }
16088
- if (owner) {
16089
- log.header(`Files owned by ${log.owner(owner)}:`);
16090
- } else {
16091
- log.header("Changed files:");
16092
- }
16093
- for (const file of files) {
16094
- log.file(file);
16095
- }
16096
- };
16097
16083
  var log = {
16098
16084
  success: (message) => console.log(source_default.green(`✓ ${message}`)),
16099
16085
  error: (message) => console.error(source_default.red(`✗ ${message}`)),
@@ -16158,15 +16144,15 @@ import { spawn as spawn2 } from "child_process";
16158
16144
  var git = esm_default();
16159
16145
  var getChangedFiles = async () => {
16160
16146
  const status = await git.status();
16161
- return status.files.filter((file) => file.working_dir !== " ").map((file) => file.path);
16147
+ return status.files.filter((file) => file.index !== " " && file.index !== "?").map((file) => file.path);
16162
16148
  };
16163
- var getStagedFiles = async () => {
16149
+ var getUnstagedFiles = async () => {
16164
16150
  const status = await git.status();
16165
- return status.files.filter((file) => file.index !== " " && file.index !== "?").map((file) => file.path);
16151
+ return status.files.filter((file) => file.working_dir !== " ").map((file) => file.path);
16166
16152
  };
16167
- var hasStagedChanges = async () => {
16168
- const stagedFiles = await getStagedFiles();
16169
- return stagedFiles.length > 0;
16153
+ var hasUnstagedChanges = async () => {
16154
+ const unstagedFiles = await getUnstagedFiles();
16155
+ return unstagedFiles.length > 0;
16170
16156
  };
16171
16157
  var branchExists = async (branchName) => {
16172
16158
  try {
@@ -16362,6 +16348,50 @@ var extractFilesFromRef = async (ref, files) => {
16362
16348
  }
16363
16349
  };
16364
16350
 
16351
+ // src/utils/matcher.ts
16352
+ var import_micromatch = __toESM(require_micromatch(), 1);
16353
+ var normalizeForMatching = (value) => {
16354
+ return value.replace(/\//g, "");
16355
+ };
16356
+ var matchOwnerPattern = (owner, patterns) => {
16357
+ if (!patterns || !patterns.trim())
16358
+ return false;
16359
+ const normalizedOwner = normalizeForMatching(owner);
16360
+ const normalizedPatterns = patterns.split(",").map((p) => normalizeForMatching(p.trim())).filter((p) => p.length > 0);
16361
+ return import_micromatch.default.isMatch(normalizedOwner, normalizedPatterns);
16362
+ };
16363
+ var matchOwners = (owners, patterns) => {
16364
+ if (!patterns || !patterns.trim())
16365
+ return false;
16366
+ const normalizedOwners = owners.map(normalizeForMatching);
16367
+ const normalizedPatterns = patterns.split(",").map((p) => normalizeForMatching(p.trim())).filter((p) => p.length > 0);
16368
+ return import_micromatch.default(normalizedOwners, normalizedPatterns).length > 0;
16369
+ };
16370
+ var matchOwnersExclusive = (owners, patterns) => {
16371
+ if (!patterns || !patterns.trim())
16372
+ return false;
16373
+ if (owners.length === 0)
16374
+ return false;
16375
+ return owners.every((owner) => matchOwnerPattern(owner, patterns));
16376
+ };
16377
+ var filterByPathPatterns = (files, pattern) => {
16378
+ if (!pattern || !pattern.trim())
16379
+ return files;
16380
+ let finalPattern = pattern.trim();
16381
+ if (finalPattern === ".") {
16382
+ finalPattern = "**";
16383
+ } else if (finalPattern.endsWith("/")) {
16384
+ finalPattern = `${finalPattern}**`;
16385
+ } else {
16386
+ const lastSegment = finalPattern.split("/").pop() || "";
16387
+ const looksLikeFile = /\.[^*?[\]{}!/]+$/.test(lastSegment);
16388
+ if (!looksLikeFile) {
16389
+ finalPattern = `${finalPattern}/**`;
16390
+ }
16391
+ }
16392
+ return import_micromatch.default(files, [finalPattern], { dot: true });
16393
+ };
16394
+
16365
16395
  // src/utils/codeowners.ts
16366
16396
  var codeowners;
16367
16397
  var getCodeownersInstance = () => {
@@ -16381,47 +16411,101 @@ var getOwner = (filePath) => {
16381
16411
  const owner = instance.getOwner(filePath);
16382
16412
  return owner;
16383
16413
  };
16384
- var getOwnerFiles = async (owner, includeUnowned = false) => {
16385
- const changedFiles = await getChangedFiles();
16414
+ var getOwnerFiles = async (ownerPattern, includeUnowned = false, pathPattern, exclusive = false, coOwned = false) => {
16415
+ let changedFiles = await getChangedFiles();
16416
+ changedFiles = filterByPathPatterns(changedFiles, pathPattern);
16386
16417
  return changedFiles.filter((file) => {
16387
16418
  const owners = getOwner(file);
16388
16419
  if (includeUnowned && owners.length === 0) {
16389
16420
  return true;
16390
16421
  }
16391
- return owners.includes(owner);
16392
- });
16393
- };
16394
-
16395
- // src/utils/matcher.ts
16396
- var import_micromatch = __toESM(require_micromatch(), 1);
16397
- var matchOwners = (owners, patterns) => {
16398
- if (!patterns || !patterns.trim())
16399
- return false;
16400
- const normalizedOwners = owners.map((owner) => {
16401
- return owner.replace(/\//g, "");
16422
+ if (coOwned && owners.length < 2) {
16423
+ return false;
16424
+ }
16425
+ if (exclusive) {
16426
+ return matchOwnersExclusive(owners, ownerPattern);
16427
+ }
16428
+ return owners.some((owner) => matchOwnerPattern(owner, ownerPattern));
16402
16429
  });
16403
- const normalizedPatterns = patterns.replace(/\//g, "").split(",");
16404
- const matches = import_micromatch.default(normalizedOwners, normalizedPatterns);
16405
- return matches.length > 0;
16406
16430
  };
16407
16431
 
16408
16432
  // src/commands/list.ts
16409
16433
  var listCodeowners = async (options) => {
16410
16434
  try {
16411
- if (options.owner) {
16412
- const files = await getOwnerFiles(options.owner);
16413
- logFileList(files, options.owner);
16414
- } else {
16415
- const changedFiles = await getChangedFiles();
16416
- const filesWithOwners = changedFiles.map((file) => ({
16417
- file,
16418
- owners: getOwner(file)
16419
- }));
16420
- let filteredFiles = filesWithOwners;
16421
- if (options.include) {
16422
- const patterns = options.include;
16423
- filteredFiles = filesWithOwners.filter(({ owners }) => matchOwners(owners, patterns));
16435
+ if (await hasUnstagedChanges()) {
16436
+ const unstagedFiles = await getUnstagedFiles();
16437
+ log.warn("Warning: Unstaged changes detected (these will be ignored):");
16438
+ unstagedFiles.forEach((file) => log.warn(` - ${file}`));
16439
+ log.info(`
16440
+ Only staged files will be processed.`);
16441
+ log.info("To stage files: git add <file>");
16442
+ log.info("");
16443
+ }
16444
+ let changedFiles = await getChangedFiles();
16445
+ changedFiles = filterByPathPatterns(changedFiles, options.pathPattern);
16446
+ const filesWithOwners = changedFiles.map((file) => ({
16447
+ file,
16448
+ owners: getOwner(file)
16449
+ }));
16450
+ let filteredFiles = filesWithOwners;
16451
+ if (options.exclusive && !options.include) {
16452
+ filteredFiles = filesWithOwners.filter(({ owners }) => owners.length === 1);
16453
+ }
16454
+ if (options.coOwned && !options.include) {
16455
+ filteredFiles = filesWithOwners.filter(({ owners }) => owners.length > 1);
16456
+ }
16457
+ if (options.include) {
16458
+ const patterns = options.include;
16459
+ const matchFn = options.exclusive ? matchOwnersExclusive : matchOwners;
16460
+ filteredFiles = filesWithOwners.filter(({ owners }) => {
16461
+ if (options.coOwned && owners.length < 2) {
16462
+ return false;
16463
+ }
16464
+ return matchFn(owners, patterns);
16465
+ });
16466
+ }
16467
+ if (options.group) {
16468
+ const ownerGroups = new Map;
16469
+ for (const { file, owners } of filteredFiles) {
16470
+ if (owners.length === 0) {
16471
+ const unownedFiles = ownerGroups.get("(unowned)") || [];
16472
+ unownedFiles.push(file);
16473
+ ownerGroups.set("(unowned)", unownedFiles);
16474
+ } else {
16475
+ for (const owner of owners) {
16476
+ const files = ownerGroups.get(owner) || [];
16477
+ files.push(file);
16478
+ ownerGroups.set(owner, files);
16479
+ }
16480
+ }
16481
+ }
16482
+ const sortedOwners = Array.from(ownerGroups.keys()).sort((a, b) => {
16483
+ if (a === "(unowned)")
16484
+ return 1;
16485
+ if (b === "(unowned)")
16486
+ return -1;
16487
+ return a.localeCompare(b);
16488
+ });
16489
+ for (const owner of sortedOwners) {
16490
+ const files = ownerGroups.get(owner) || [];
16491
+ const tableData = files.map((file, index) => ({
16492
+ No: index + 1,
16493
+ File: file
16494
+ }));
16495
+ log.header(`Files owned by ${log.owner(owner)}:`);
16496
+ log.formattedTable(tableData, [
16497
+ {
16498
+ name: "No",
16499
+ width: 8
16500
+ },
16501
+ {
16502
+ name: "File",
16503
+ width: 100,
16504
+ formatter: (value) => log.smartFile(value)
16505
+ }
16506
+ ]);
16424
16507
  }
16508
+ } else {
16425
16509
  const tableData = filteredFiles.map(({ file, owners }, index) => ({
16426
16510
  No: index + 1,
16427
16511
  File: file,
@@ -16550,6 +16634,9 @@ var createPRWithTemplate = async (title, branchName, options = {}) => {
16550
16634
  });
16551
16635
  };
16552
16636
 
16637
+ // src/commands/branch.ts
16638
+ var import_cli_table32 = __toESM(require_table(), 1);
16639
+
16553
16640
  // src/utils/state.ts
16554
16641
  import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync } from "fs";
16555
16642
  import { join } from "path";
@@ -16694,8 +16781,9 @@ var branch = async (options) => {
16694
16781
  let pushed = false;
16695
16782
  let operationState = options.operationState || null;
16696
16783
  const isSubOperation = !!options.operationState;
16784
+ let autoRecoverySucceeded = false;
16697
16785
  try {
16698
- if (!options.branch || !options.message || !options.owner) {
16786
+ if (!options.branch || !options.message || !options.include) {
16699
16787
  throw new Error("Missing required options for branch creation");
16700
16788
  }
16701
16789
  if ((options.pr || options.draftPr) && !options.push) {
@@ -16704,19 +16792,14 @@ var branch = async (options) => {
16704
16792
  if (options.pr && options.draftPr) {
16705
16793
  throw new Error("Cannot use both --pr and --draft-pr options");
16706
16794
  }
16707
- if (await hasStagedChanges()) {
16708
- const stagedFiles = await getStagedFiles();
16709
- log.error("Changes need to be unstaged in order for this to work.");
16710
- log.info(`
16711
- Staged files detected:`);
16712
- stagedFiles.forEach((file) => log.info(` - ${file}`));
16713
- log.info(`
16714
- To unstage files, run:`);
16715
- log.info(" git restore --staged .");
16795
+ if (await hasUnstagedChanges()) {
16796
+ const unstagedFiles = await getUnstagedFiles();
16797
+ log.warn("Warning: Unstaged changes detected (these will be ignored):");
16798
+ unstagedFiles.forEach((file) => log.warn(` - ${file}`));
16716
16799
  log.info(`
16717
- Or to unstage specific files:`);
16718
- log.info(" git restore --staged <file>");
16719
- throw new Error("Staged changes detected. Please unstage all changes before running this command.");
16800
+ Only staged files will be processed.`);
16801
+ log.info("To stage files: git add <file>");
16802
+ log.info("");
16720
16803
  }
16721
16804
  log.info(options.append ? "Starting branch update process..." : "Starting branch creation process...");
16722
16805
  originalBranch = await getCurrentBranch();
@@ -16733,13 +16816,13 @@ Or to unstage specific files:`);
16733
16816
  });
16734
16817
  log.info(`Operation ID: ${operationState.id}`);
16735
16818
  }
16736
- filesToCommit = await getOwnerFiles(options.owner, options.isDefaultOwner || false);
16819
+ filesToCommit = await getOwnerFiles(options.include, options.isDefaultOwner || false, options.pathPattern, options.exclusive || false, options.coOwned || false);
16737
16820
  if (filesToCommit.length <= 0) {
16738
- log.warn(`No files found for ${options.owner}. Skipping branch creation.`);
16821
+ log.warn(`No files found for ${options.include}. Skipping branch creation.`);
16739
16822
  return {
16740
16823
  success: false,
16741
16824
  branchName: options.branch,
16742
- owner: options.owner,
16825
+ owner: options.include,
16743
16826
  files: [],
16744
16827
  pushed: false,
16745
16828
  error: "No files found for this owner"
@@ -16759,7 +16842,7 @@ Or to unstage specific files:`);
16759
16842
  });
16760
16843
  updateBranchState(operationState.id, options.branch, {
16761
16844
  name: options.branch,
16762
- owner: options.owner || "",
16845
+ owner: options.include || "",
16763
16846
  files: filesToCommit,
16764
16847
  created: false,
16765
16848
  committed: false,
@@ -16848,15 +16931,30 @@ Or to unstage specific files:`);
16848
16931
  if (operationState && !isSubOperation) {
16849
16932
  completeOperation(operationState.id, true);
16850
16933
  }
16851
- if (branchAlreadyExists && options.append) {
16852
- log.success(options.push ? `Changes committed to existing branch "${options.branch}" and pushed to remote.` : `Changes committed to existing branch "${options.branch}".`);
16853
- } else {
16854
- log.success(options.push ? `Branch "${options.branch}" created, changes committed, and pushed to remote.` : `Branch "${options.branch}" created and changes committed.`);
16934
+ if (!isSubOperation) {
16935
+ log.header(options.append ? "Branch update summary" : "Branch creation summary");
16936
+ const table = new import_cli_table32.default({
16937
+ head: ["Status", "Owner", "Branch", "Files", "Pushed", "PR"],
16938
+ colWidths: [10, 25, 35, 10, 10, 50],
16939
+ wordWrap: true
16940
+ });
16941
+ table.push([
16942
+ "✓",
16943
+ options.include,
16944
+ options.branch,
16945
+ `${filesToCommit.length} file${filesToCommit.length !== 1 ? "s" : ""}`,
16946
+ pushed ? "✓" : "-",
16947
+ prUrl || "-"
16948
+ ]);
16949
+ console.log(table.toString());
16950
+ console.log(`
16951
+ Files committed:`);
16952
+ filesToCommit.forEach((file) => console.log(` - ${file}`));
16855
16953
  }
16856
16954
  return {
16857
16955
  success: true,
16858
16956
  branchName: options.branch,
16859
- owner: options.owner,
16957
+ owner: options.include,
16860
16958
  files: filesToCommit,
16861
16959
  pushed,
16862
16960
  prUrl,
@@ -16902,6 +17000,17 @@ Or to unstage specific files:`);
16902
17000
  }
16903
17001
  } catch (cleanupError) {
16904
17002
  log.error(`Error during cleanup: ${cleanupError}`);
17003
+ throw operationError;
17004
+ }
17005
+ autoRecoverySucceeded = true;
17006
+ log.success("Auto-recovery completed successfully");
17007
+ if (operationState && !isSubOperation) {
17008
+ deleteOperationState(operationState.id);
17009
+ }
17010
+ } else {
17011
+ autoRecoverySucceeded = true;
17012
+ if (operationState && !isSubOperation) {
17013
+ deleteOperationState(operationState.id);
16905
17014
  }
16906
17015
  }
16907
17016
  throw operationError;
@@ -16912,7 +17021,7 @@ Or to unstage specific files:`);
16912
17021
  return {
16913
17022
  success: false,
16914
17023
  branchName: options.branch ?? "",
16915
- owner: options.owner ?? "",
17024
+ owner: options.include ?? "",
16916
17025
  files: filesToCommit,
16917
17026
  pushed,
16918
17027
  prUrl,
@@ -16920,9 +17029,9 @@ Or to unstage specific files:`);
16920
17029
  error: String(err)
16921
17030
  };
16922
17031
  }
16923
- if (operationState) {
17032
+ if (operationState && !autoRecoverySucceeded) {
16924
17033
  log.info(`
16925
- Recovery options:`);
17034
+ Auto-recovery failed. Manual recovery options:`);
16926
17035
  log.info(` 1. Run 'codeowners-git recover --id ${operationState.id}' to clean up and return to original branch`);
16927
17036
  log.info(` 2. Run 'codeowners-git recover --id ${operationState.id} --keep-branches' to return without deleting branches`);
16928
17037
  log.info(` 3. Run 'codeowners-git recover --list' to see all incomplete operations`);
@@ -16943,422 +17052,157 @@ Recovery options:`);
16943
17052
  }
16944
17053
  };
16945
17054
 
16946
- // src/commands/multi-branch.ts
16947
- var import_micromatch2 = __toESM(require_micromatch(), 1);
16948
- var import_cli_table32 = __toESM(require_table(), 1);
16949
- var multiBranch = async (options) => {
16950
- let operationState = null;
16951
- try {
16952
- if (!options.branch || !options.message) {
16953
- throw new Error("Missing required options for multi-branch creation");
16954
- }
16955
- if (options.ignore && options.include) {
16956
- throw new Error("Cannot use both --ignore and --include options at the same time");
16957
- }
16958
- if ((options.pr || options.draftPr) && !options.push) {
16959
- throw new Error("Pull request creation requires --push option");
16960
- }
16961
- if (options.pr && options.draftPr) {
16962
- throw new Error("Cannot use both --pr and --draft-pr options");
17055
+ // node_modules/@inquirer/core/dist/esm/lib/key.js
17056
+ var isUpKey = (key) => key.name === "up" || key.name === "k" || key.ctrl && key.name === "p";
17057
+ var isDownKey = (key) => key.name === "down" || key.name === "j" || key.ctrl && key.name === "n";
17058
+ var isBackspaceKey = (key) => key.name === "backspace";
17059
+ var isNumberKey = (key) => "123456789".includes(key.name);
17060
+ var isEnterKey = (key) => key.name === "enter" || key.name === "return";
17061
+ // node_modules/@inquirer/core/dist/esm/lib/errors.js
17062
+ class AbortPromptError extends Error {
17063
+ name = "AbortPromptError";
17064
+ message = "Prompt was aborted";
17065
+ constructor(options) {
17066
+ super();
17067
+ this.cause = options?.cause;
17068
+ }
17069
+ }
17070
+
17071
+ class CancelPromptError extends Error {
17072
+ name = "CancelPromptError";
17073
+ message = "Prompt was canceled";
17074
+ }
17075
+
17076
+ class ExitPromptError extends Error {
17077
+ name = "ExitPromptError";
17078
+ }
17079
+
17080
+ class HookError extends Error {
17081
+ name = "HookError";
17082
+ }
17083
+
17084
+ class ValidationError extends Error {
17085
+ name = "ValidationError";
17086
+ }
17087
+ // node_modules/@inquirer/core/dist/esm/lib/use-prefix.js
17088
+ import { AsyncResource as AsyncResource2 } from "node:async_hooks";
17089
+
17090
+ // node_modules/@inquirer/core/dist/esm/lib/hook-engine.js
17091
+ import { AsyncLocalStorage, AsyncResource } from "node:async_hooks";
17092
+ var hookStorage = new AsyncLocalStorage;
17093
+ function createStore(rl) {
17094
+ const store = {
17095
+ rl,
17096
+ hooks: [],
17097
+ hooksCleanup: [],
17098
+ hooksEffect: [],
17099
+ index: 0,
17100
+ handleChange() {}
17101
+ };
17102
+ return store;
17103
+ }
17104
+ function withHooks(rl, cb) {
17105
+ const store = createStore(rl);
17106
+ return hookStorage.run(store, () => {
17107
+ function cycle(render) {
17108
+ store.handleChange = () => {
17109
+ store.index = 0;
17110
+ render();
17111
+ };
17112
+ store.handleChange();
16963
17113
  }
16964
- if (await hasStagedChanges()) {
16965
- const stagedFiles = await getStagedFiles();
16966
- log.error("Changes need to be unstaged in order for this to work.");
16967
- log.info(`
16968
- Staged files detected:`);
16969
- stagedFiles.forEach((file) => log.info(` - ${file}`));
16970
- log.info(`
16971
- To unstage files, run:`);
16972
- log.info(" git restore --staged .");
16973
- log.info(`
16974
- Or to unstage specific files:`);
16975
- log.info(" git restore --staged <file>");
16976
- throw new Error("Staged changes detected. Please unstage all changes before running this command.");
17114
+ return cb(cycle);
17115
+ });
17116
+ }
17117
+ function getStore() {
17118
+ const store = hookStorage.getStore();
17119
+ if (!store) {
17120
+ throw new HookError("[Inquirer] Hook functions can only be called from within a prompt");
17121
+ }
17122
+ return store;
17123
+ }
17124
+ function readline() {
17125
+ return getStore().rl;
17126
+ }
17127
+ function withUpdates(fn) {
17128
+ const wrapped = (...args) => {
17129
+ const store = getStore();
17130
+ let shouldUpdate = false;
17131
+ const oldHandleChange = store.handleChange;
17132
+ store.handleChange = () => {
17133
+ shouldUpdate = true;
17134
+ };
17135
+ const returnValue = fn(...args);
17136
+ if (shouldUpdate) {
17137
+ oldHandleChange();
16977
17138
  }
16978
- log.info(options.append ? "Starting multi-branch update process..." : "Starting multi-branch creation process...");
16979
- const originalBranch = await getCurrentBranch();
16980
- operationState = createOperationState("multi-branch", originalBranch, {
16981
- verify: options.verify,
16982
- push: options.push,
16983
- remote: options.remote,
16984
- force: options.force,
16985
- keepBranchOnFailure: options.keepBranchOnFailure,
16986
- pr: options.pr,
16987
- draftPr: options.draftPr
17139
+ store.handleChange = oldHandleChange;
17140
+ return returnValue;
17141
+ };
17142
+ return AsyncResource.bind(wrapped);
17143
+ }
17144
+ function withPointer(cb) {
17145
+ const store = getStore();
17146
+ const { index } = store;
17147
+ const pointer = {
17148
+ get() {
17149
+ return store.hooks[index];
17150
+ },
17151
+ set(value) {
17152
+ store.hooks[index] = value;
17153
+ },
17154
+ initialized: index in store.hooks
17155
+ };
17156
+ const returnValue = cb(pointer);
17157
+ store.index++;
17158
+ return returnValue;
17159
+ }
17160
+ function handleChange() {
17161
+ getStore().handleChange();
17162
+ }
17163
+ var effectScheduler = {
17164
+ queue(cb) {
17165
+ const store = getStore();
17166
+ const { index } = store;
17167
+ store.hooksEffect.push(() => {
17168
+ store.hooksCleanup[index]?.();
17169
+ const cleanFn = cb(readline());
17170
+ if (cleanFn != null && typeof cleanFn !== "function") {
17171
+ throw new ValidationError("useEffect return value must be a cleanup function or nothing.");
17172
+ }
17173
+ store.hooksCleanup[index] = cleanFn;
16988
17174
  });
16989
- log.info(`Operation ID: ${operationState.id}`);
16990
- const changedFiles = await getChangedFiles();
16991
- if (changedFiles.length === 0) {
16992
- throw new Error("No changed files found in the repository");
16993
- }
16994
- const ownerSet = new Set;
16995
- const filesWithoutOwners = [];
16996
- for (const file of changedFiles) {
16997
- const owners = getOwner(file);
16998
- if (owners.length === 0) {
16999
- filesWithoutOwners.push(file);
17000
- } else {
17001
- for (const owner of owners) {
17002
- ownerSet.add(owner);
17003
- }
17004
- }
17005
- }
17006
- let codeowners2 = Array.from(ownerSet);
17007
- if (filesWithoutOwners.length > 0 && options.defaultOwner) {
17008
- log.info(`Found ${filesWithoutOwners.length} files without owners. Adding default owner: ${options.defaultOwner}`);
17009
- codeowners2.push(options.defaultOwner);
17010
- }
17011
- if (codeowners2.length === 0) {
17012
- log.warn("No codeowners found for the changed files");
17013
- log.info("Continuing without creating any branches (use --default-owner to specify a fallback)");
17014
- return;
17015
- } else {
17016
- log.info(`Found ${codeowners2.length} codeowners: ${codeowners2.join(", ")}`);
17017
- }
17018
- if (options.ignore || options.include) {
17019
- const originalCount = codeowners2.length;
17020
- if (options.ignore) {
17021
- const ignorePatterns = options.ignore.split(",").map((p) => p.trim());
17022
- codeowners2 = codeowners2.filter((owner) => !import_micromatch2.default.isMatch(owner, ignorePatterns));
17023
- log.info(`Filtered out ${originalCount - codeowners2.length} codeowners using ignore patterns: ${ignorePatterns.join(", ")}`);
17024
- } else if (options.include) {
17025
- const includePatterns = options.include.split(",").map((p) => p.trim());
17026
- codeowners2 = codeowners2.filter((owner) => import_micromatch2.default.isMatch(owner, includePatterns));
17027
- log.info(`Filtered to ${codeowners2.length} codeowners using include patterns: ${includePatterns.join(", ")}`);
17028
- }
17029
- if (codeowners2.length === 0) {
17030
- log.warn("No codeowners left after filtering");
17031
- return;
17032
- }
17033
- log.info(`Processing ${codeowners2.length} codeowners after filtering: ${codeowners2.join(", ")}`);
17034
- }
17035
- const results = [];
17036
- for (const owner of codeowners2) {
17037
- const sanitizedOwner = owner.replace(/[^a-zA-Z0-9-_@]/g, "-").replace(/^@/, "");
17038
- const branchName = `${options.branch}/${sanitizedOwner}`;
17039
- const commitMessage = `${options.message} - ${owner}`;
17040
- log.info(options.append ? `Updating branch for ${owner}...` : `Creating branch for ${owner}...`);
17041
- const result = await branch({
17042
- owner,
17043
- branch: branchName,
17044
- message: commitMessage,
17045
- verify: options.verify,
17046
- push: options.push,
17047
- remote: options.remote,
17048
- upstream: options.upstream,
17049
- force: options.force,
17050
- keepBranchOnFailure: options.keepBranchOnFailure,
17051
- isDefaultOwner: owner === options.defaultOwner,
17052
- append: options.append,
17053
- pr: options.pr,
17054
- draftPr: options.draftPr,
17055
- operationState: operationState || undefined
17056
- });
17057
- results.push(result);
17058
- }
17059
- const successCount = results.filter((r) => r.success).length;
17060
- const failureCount = results.filter((r) => !r.success).length;
17061
- log.header(options.append ? "Multi-branch update summary" : "Multi-branch creation summary");
17062
- log.info(options.append ? `Successfully updated ${successCount} of ${codeowners2.length} branches` : `Successfully created ${successCount} of ${codeowners2.length} branches`);
17063
- if (failureCount > 0) {
17064
- log.error(`Failed: ${failureCount} branches`);
17065
- }
17066
- console.log("");
17067
- const table = new import_cli_table32.default({
17068
- head: ["Status", "Owner", "Branch", "Files", "Pushed", "PR"],
17069
- colWidths: [10, 20, 40, 10, 10, 50],
17070
- wordWrap: true
17071
- });
17072
- for (const result of results) {
17073
- const status = result.success ? "✓" : "✗";
17074
- const fileCount = result.files.length;
17075
- const pushedStatus = result.pushed ? "✓" : "-";
17076
- const prInfo = result.prUrl ? result.prUrl : result.error && result.error.includes("PR creation failed") ? "Failed" : "-";
17077
- table.push([
17078
- status,
17079
- result.owner,
17080
- result.branchName,
17081
- `${fileCount} file${fileCount !== 1 ? "s" : ""}`,
17082
- pushedStatus,
17083
- prInfo
17084
- ]);
17085
- }
17086
- console.log(table.toString());
17087
- if (results.length > 0) {
17088
- console.log(`
17089
- Files by branch:`);
17090
- for (const result of results) {
17091
- if (result.success && result.files.length > 0) {
17092
- console.log(`
17093
- ${result.branchName} (${result.owner}):`);
17094
- result.files.forEach((file) => console.log(` - ${file}`));
17095
- }
17096
- }
17097
- }
17098
- const errors = results.filter((r) => r.error);
17099
- if (errors.length > 0) {
17100
- console.log(`
17101
- Errors:`);
17102
- errors.forEach((result) => {
17103
- console.log(`
17104
- ${result.branchName} (${result.owner}):`);
17105
- console.log(` ${result.error}`);
17106
- });
17107
- }
17108
- if (operationState) {
17109
- completeOperation(operationState.id, true);
17110
- }
17111
- } catch (err) {
17112
- log.error(`Multi-branch operation failed: ${err}`);
17113
- if (operationState) {
17114
- failOperation(operationState.id, String(err));
17115
- log.info(`
17116
- Recovery options:`);
17117
- log.info(` 1. Run 'codeowners-git recover --id ${operationState.id}' to clean up and return to original branch`);
17118
- log.info(` 2. Run 'codeowners-git recover --id ${operationState.id} --keep-branches' to return without deleting branches`);
17119
- log.info(` 3. Run 'codeowners-git recover --list' to see all incomplete operations`);
17120
- }
17121
- process.exit(1);
17122
- }
17123
- };
17124
-
17125
- // src/commands/extract.ts
17126
- var import_micromatch3 = __toESM(require_micromatch(), 1);
17127
- var extract = async (options) => {
17128
- try {
17129
- if (!options.source) {
17130
- log.error("Missing required option: --source");
17131
- log.info(`
17132
- Usage:`);
17133
- log.info(" cg extract -s <branch-or-commit>");
17134
- log.info(`
17135
- Examples:`);
17136
- log.info(" cg extract -s feature/other-branch");
17137
- log.info(" cg extract -s feature/other-branch -o '@team-*'");
17138
- log.info(" cg extract -s abc123def --compare-main");
17139
- process.exit(1);
17140
- }
17141
- if (await hasStagedChanges()) {
17142
- const stagedFiles = await getStagedFiles();
17143
- log.error("Changes need to be unstaged in order for this to work.");
17144
- log.info(`
17145
- Staged files detected:`);
17146
- stagedFiles.forEach((file) => log.info(` - ${file}`));
17147
- log.info(`
17148
- To unstage files, run:`);
17149
- log.info(" git restore --staged .");
17150
- log.info(`
17151
- Or to unstage specific files:`);
17152
- log.info(" git restore --staged <file>");
17153
- process.exit(1);
17154
- }
17155
- log.info("Starting file extraction process...");
17156
- let compareTarget;
17157
- if (options.compareMain) {
17158
- compareTarget = await getDefaultBranch();
17159
- log.info(`Comparing ${options.source} against ${compareTarget}...`);
17160
- } else {
17161
- const baseBranch = await getBaseBranch(options.source);
17162
- compareTarget = baseBranch;
17163
- log.info(`Detected base branch: ${baseBranch}`);
17164
- log.info(`Extracting changes from ${options.source}...`);
17165
- }
17166
- const changedFiles = await getChangedFilesBetween(options.source, compareTarget);
17167
- if (changedFiles.length === 0) {
17168
- log.warn(`No changed files found in ${options.source}`);
17169
- return;
17170
- }
17171
- log.info(`Found ${changedFiles.length} changed file${changedFiles.length !== 1 ? "s" : ""}`);
17172
- let filesToExtract = changedFiles;
17173
- if (options.owner) {
17174
- log.info(`Filtering files by owner pattern: ${options.owner}`);
17175
- const ownedFiles = [];
17176
- for (const file of changedFiles) {
17177
- const owners = await getOwner(file);
17178
- if (owners.length > 0) {
17179
- const matchingOwners = import_micromatch3.default(owners, options.owner);
17180
- if (matchingOwners.length > 0) {
17181
- ownedFiles.push(file);
17182
- }
17183
- }
17184
- }
17185
- filesToExtract = ownedFiles;
17186
- if (filesToExtract.length === 0) {
17187
- log.warn(`No files match the owner pattern: ${options.owner}`);
17188
- return;
17189
- }
17190
- log.info(`Filtered to ${filesToExtract.length} file${filesToExtract.length !== 1 ? "s" : ""}`);
17191
- }
17192
- log.info("Extracting files to working directory...");
17193
- await extractFilesFromRef(options.source, filesToExtract);
17194
- log.success(`
17195
- ✓ Extracted ${filesToExtract.length} file${filesToExtract.length !== 1 ? "s" : ""} to working directory (unstaged)`);
17196
- log.info(`
17197
- Extracted files:`);
17198
- filesToExtract.forEach((file) => log.info(` - ${file}`));
17199
- log.info(`
17200
- Next steps:`);
17201
- log.info(" - Review the extracted files in your working directory");
17202
- log.info(" - Use 'cg branch' command to create a branch and commit");
17203
- log.info(" - Example: cg branch -o @my-team -b my-branch -m 'Commit message' -p");
17204
- } catch (err) {
17205
- log.error(`
17206
- ✗ Extraction failed: ${err}`);
17207
- process.exit(1);
17208
- }
17209
- };
17210
-
17211
- // node_modules/@inquirer/core/dist/esm/lib/key.js
17212
- var isUpKey = (key) => key.name === "up" || key.name === "k" || key.ctrl && key.name === "p";
17213
- var isDownKey = (key) => key.name === "down" || key.name === "j" || key.ctrl && key.name === "n";
17214
- var isBackspaceKey = (key) => key.name === "backspace";
17215
- var isNumberKey = (key) => "123456789".includes(key.name);
17216
- var isEnterKey = (key) => key.name === "enter" || key.name === "return";
17217
- // node_modules/@inquirer/core/dist/esm/lib/errors.js
17218
- class AbortPromptError extends Error {
17219
- name = "AbortPromptError";
17220
- message = "Prompt was aborted";
17221
- constructor(options) {
17222
- super();
17223
- this.cause = options?.cause;
17224
- }
17225
- }
17226
-
17227
- class CancelPromptError extends Error {
17228
- name = "CancelPromptError";
17229
- message = "Prompt was canceled";
17230
- }
17231
-
17232
- class ExitPromptError extends Error {
17233
- name = "ExitPromptError";
17234
- }
17235
-
17236
- class HookError extends Error {
17237
- name = "HookError";
17238
- }
17239
-
17240
- class ValidationError extends Error {
17241
- name = "ValidationError";
17242
- }
17243
- // node_modules/@inquirer/core/dist/esm/lib/use-prefix.js
17244
- import { AsyncResource as AsyncResource2 } from "node:async_hooks";
17245
-
17246
- // node_modules/@inquirer/core/dist/esm/lib/hook-engine.js
17247
- import { AsyncLocalStorage, AsyncResource } from "node:async_hooks";
17248
- var hookStorage = new AsyncLocalStorage;
17249
- function createStore(rl) {
17250
- const store = {
17251
- rl,
17252
- hooks: [],
17253
- hooksCleanup: [],
17254
- hooksEffect: [],
17255
- index: 0,
17256
- handleChange() {}
17257
- };
17258
- return store;
17259
- }
17260
- function withHooks(rl, cb) {
17261
- const store = createStore(rl);
17262
- return hookStorage.run(store, () => {
17263
- function cycle(render) {
17264
- store.handleChange = () => {
17265
- store.index = 0;
17266
- render();
17267
- };
17268
- store.handleChange();
17269
- }
17270
- return cb(cycle);
17271
- });
17272
- }
17273
- function getStore() {
17274
- const store = hookStorage.getStore();
17275
- if (!store) {
17276
- throw new HookError("[Inquirer] Hook functions can only be called from within a prompt");
17277
- }
17278
- return store;
17279
- }
17280
- function readline() {
17281
- return getStore().rl;
17282
- }
17283
- function withUpdates(fn) {
17284
- const wrapped = (...args) => {
17285
- const store = getStore();
17286
- let shouldUpdate = false;
17287
- const oldHandleChange = store.handleChange;
17288
- store.handleChange = () => {
17289
- shouldUpdate = true;
17290
- };
17291
- const returnValue = fn(...args);
17292
- if (shouldUpdate) {
17293
- oldHandleChange();
17294
- }
17295
- store.handleChange = oldHandleChange;
17296
- return returnValue;
17297
- };
17298
- return AsyncResource.bind(wrapped);
17299
- }
17300
- function withPointer(cb) {
17301
- const store = getStore();
17302
- const { index } = store;
17303
- const pointer = {
17304
- get() {
17305
- return store.hooks[index];
17306
- },
17307
- set(value) {
17308
- store.hooks[index] = value;
17309
- },
17310
- initialized: index in store.hooks
17311
- };
17312
- const returnValue = cb(pointer);
17313
- store.index++;
17314
- return returnValue;
17315
- }
17316
- function handleChange() {
17317
- getStore().handleChange();
17318
- }
17319
- var effectScheduler = {
17320
- queue(cb) {
17321
- const store = getStore();
17322
- const { index } = store;
17323
- store.hooksEffect.push(() => {
17324
- store.hooksCleanup[index]?.();
17325
- const cleanFn = cb(readline());
17326
- if (cleanFn != null && typeof cleanFn !== "function") {
17327
- throw new ValidationError("useEffect return value must be a cleanup function or nothing.");
17328
- }
17329
- store.hooksCleanup[index] = cleanFn;
17330
- });
17331
- },
17332
- run() {
17333
- const store = getStore();
17334
- withUpdates(() => {
17335
- store.hooksEffect.forEach((effect) => {
17336
- effect();
17337
- });
17338
- store.hooksEffect.length = 0;
17339
- })();
17340
- },
17341
- clearAll() {
17342
- const store = getStore();
17343
- store.hooksCleanup.forEach((cleanFn) => {
17344
- cleanFn?.();
17345
- });
17346
- store.hooksEffect.length = 0;
17347
- store.hooksCleanup.length = 0;
17348
- }
17349
- };
17350
-
17351
- // node_modules/@inquirer/core/dist/esm/lib/use-state.js
17352
- function useState(defaultValue) {
17353
- return withPointer((pointer) => {
17354
- const setFn = (newValue) => {
17355
- if (pointer.get() !== newValue) {
17356
- pointer.set(newValue);
17357
- handleChange();
17358
- }
17359
- };
17360
- if (pointer.initialized) {
17361
- return [pointer.get(), setFn];
17175
+ },
17176
+ run() {
17177
+ const store = getStore();
17178
+ withUpdates(() => {
17179
+ store.hooksEffect.forEach((effect) => {
17180
+ effect();
17181
+ });
17182
+ store.hooksEffect.length = 0;
17183
+ })();
17184
+ },
17185
+ clearAll() {
17186
+ const store = getStore();
17187
+ store.hooksCleanup.forEach((cleanFn) => {
17188
+ cleanFn?.();
17189
+ });
17190
+ store.hooksEffect.length = 0;
17191
+ store.hooksCleanup.length = 0;
17192
+ }
17193
+ };
17194
+
17195
+ // node_modules/@inquirer/core/dist/esm/lib/use-state.js
17196
+ function useState(defaultValue) {
17197
+ return withPointer((pointer) => {
17198
+ const setFn = (newValue) => {
17199
+ if (pointer.get() !== newValue) {
17200
+ pointer.set(newValue);
17201
+ handleChange();
17202
+ }
17203
+ };
17204
+ if (pointer.initialized) {
17205
+ return [pointer.get(), setFn];
17362
17206
  }
17363
17207
  const value = typeof defaultValue === "function" ? defaultValue() : defaultValue;
17364
17208
  pointer.set(value);
@@ -18497,6 +18341,7 @@ Operation ID: ${op.id}`);
18497
18341
  };
18498
18342
  var performRecovery = async (state, keepBranches) => {
18499
18343
  log.header(`Recovering from operation ${state.id}`);
18344
+ let hadWarnings = false;
18500
18345
  const currentBranch = await getCurrentBranch();
18501
18346
  if (currentBranch !== state.originalBranch) {
18502
18347
  try {
@@ -18508,8 +18353,30 @@ var performRecovery = async (state, keepBranches) => {
18508
18353
  log.info(`You may need to manually run: git checkout ${state.originalBranch}`);
18509
18354
  throw error;
18510
18355
  }
18511
- } else {
18512
- log.info(`Already on original branch: ${state.originalBranch}`);
18356
+ } else {
18357
+ log.info(`Already on original branch: ${state.originalBranch}`);
18358
+ }
18359
+ if (!keepBranches && state.branches.length > 0) {
18360
+ log.info(`
18361
+ Restoring files from branches...`);
18362
+ for (const branch2 of state.branches) {
18363
+ if (branch2.committed && branch2.files && branch2.files.length > 0) {
18364
+ try {
18365
+ const exists2 = await branchExists(branch2.name);
18366
+ if (exists2) {
18367
+ log.info(`Restoring ${branch2.files.length} file(s) from ${branch2.name}...`);
18368
+ await restoreFilesFromBranch(branch2.name, branch2.files);
18369
+ } else {
18370
+ log.warn(`Branch ${branch2.name} no longer exists, cannot restore files`);
18371
+ hadWarnings = true;
18372
+ }
18373
+ } catch (error) {
18374
+ log.error(`Failed to restore files from ${branch2.name}: ${error}`);
18375
+ log.info(`Files may still be accessible from the branch if it exists`);
18376
+ hadWarnings = true;
18377
+ }
18378
+ }
18379
+ }
18513
18380
  }
18514
18381
  if (!keepBranches && state.branches.length > 0) {
18515
18382
  log.info(`
@@ -18527,6 +18394,7 @@ Cleaning up created branches...`);
18527
18394
  } catch (error) {
18528
18395
  log.error(`Failed to delete branch ${branch2.name}: ${error}`);
18529
18396
  log.info(`You may need to manually run: git branch -D ${branch2.name}`);
18397
+ hadWarnings = true;
18530
18398
  }
18531
18399
  }
18532
18400
  }
@@ -18545,6 +18413,7 @@ Cleaning up state file...`);
18545
18413
  log.success(`State file deleted: ${state.id}`);
18546
18414
  log.success(`
18547
18415
  ✓ Recovery complete!`);
18416
+ return !hadWarnings;
18548
18417
  };
18549
18418
  var recover = async (options) => {
18550
18419
  try {
@@ -18606,8 +18475,311 @@ Operation details:`);
18606
18475
  process.exit(1);
18607
18476
  }
18608
18477
  };
18478
+
18479
+ // src/commands/multi-branch.ts
18480
+ var import_cli_table33 = __toESM(require_table(), 1);
18481
+ var multiBranch = async (options) => {
18482
+ let operationState = null;
18483
+ try {
18484
+ if (!options.branch || !options.message) {
18485
+ throw new Error("Missing required options for multi-branch creation");
18486
+ }
18487
+ if (options.ignore && options.include) {
18488
+ throw new Error("Cannot use both --ignore and --include options at the same time");
18489
+ }
18490
+ if ((options.pr || options.draftPr) && !options.push) {
18491
+ throw new Error("Pull request creation requires --push option");
18492
+ }
18493
+ if (options.pr && options.draftPr) {
18494
+ throw new Error("Cannot use both --pr and --draft-pr options");
18495
+ }
18496
+ if (await hasUnstagedChanges()) {
18497
+ const unstagedFiles = await getUnstagedFiles();
18498
+ log.warn("Warning: Unstaged changes detected (these will be ignored):");
18499
+ unstagedFiles.forEach((file) => log.warn(` - ${file}`));
18500
+ log.info(`
18501
+ Only staged files will be processed.`);
18502
+ log.info("To stage files: git add <file>");
18503
+ log.info("");
18504
+ }
18505
+ log.info(options.append ? "Starting multi-branch update process..." : "Starting multi-branch creation process...");
18506
+ const originalBranch = await getCurrentBranch();
18507
+ operationState = createOperationState("multi-branch", originalBranch, {
18508
+ verify: options.verify,
18509
+ push: options.push,
18510
+ remote: options.remote,
18511
+ force: options.force,
18512
+ keepBranchOnFailure: options.keepBranchOnFailure,
18513
+ pr: options.pr,
18514
+ draftPr: options.draftPr
18515
+ });
18516
+ log.info(`Operation ID: ${operationState.id}`);
18517
+ let changedFiles = await getChangedFiles();
18518
+ changedFiles = filterByPathPatterns(changedFiles, options.pathPattern);
18519
+ if (changedFiles.length === 0) {
18520
+ throw new Error(options.pathPattern ? `No changed files found matching pattern: ${options.pathPattern}` : "No changed files found in the repository");
18521
+ }
18522
+ const ownerSet = new Set;
18523
+ const filesWithoutOwners = [];
18524
+ for (const file of changedFiles) {
18525
+ const owners = getOwner(file);
18526
+ if (owners.length === 0) {
18527
+ filesWithoutOwners.push(file);
18528
+ } else {
18529
+ for (const owner of owners) {
18530
+ ownerSet.add(owner);
18531
+ }
18532
+ }
18533
+ }
18534
+ let codeowners2 = Array.from(ownerSet);
18535
+ if (filesWithoutOwners.length > 0 && options.defaultOwner) {
18536
+ log.info(`Found ${filesWithoutOwners.length} files without owners. Adding default owner: ${options.defaultOwner}`);
18537
+ codeowners2.push(options.defaultOwner);
18538
+ }
18539
+ if (codeowners2.length === 0) {
18540
+ log.warn("No codeowners found for the changed files");
18541
+ log.info("Continuing without creating any branches (use --default-owner to specify a fallback)");
18542
+ return;
18543
+ } else {
18544
+ log.info(`Found ${codeowners2.length} codeowners: ${codeowners2.join(", ")}`);
18545
+ }
18546
+ if (options.ignore || options.include) {
18547
+ const originalCount = codeowners2.length;
18548
+ if (options.ignore) {
18549
+ codeowners2 = codeowners2.filter((owner) => !matchOwnerPattern(owner, options.ignore));
18550
+ log.info(`Filtered out ${originalCount - codeowners2.length} codeowners using ignore patterns: ${options.ignore}`);
18551
+ } else if (options.include) {
18552
+ codeowners2 = codeowners2.filter((owner) => matchOwnerPattern(owner, options.include));
18553
+ log.info(`Filtered to ${codeowners2.length} codeowners using include patterns: ${options.include}`);
18554
+ }
18555
+ if (codeowners2.length === 0) {
18556
+ log.warn("No codeowners left after filtering");
18557
+ return;
18558
+ }
18559
+ log.info(`Processing ${codeowners2.length} codeowners after filtering: ${codeowners2.join(", ")}`);
18560
+ }
18561
+ const results = [];
18562
+ for (const owner of codeowners2) {
18563
+ const sanitizedOwner = owner.replace(/[^a-zA-Z0-9-_@]/g, "-").replace(/^@/, "");
18564
+ const branchName = `${options.branch}/${sanitizedOwner}`;
18565
+ const commitMessage = `${options.message} - ${owner}`;
18566
+ log.info(options.append ? `Updating branch for ${owner}...` : `Creating branch for ${owner}...`);
18567
+ const result = await branch({
18568
+ include: owner,
18569
+ branch: branchName,
18570
+ message: commitMessage,
18571
+ verify: options.verify,
18572
+ push: options.push,
18573
+ remote: options.remote,
18574
+ upstream: options.upstream,
18575
+ force: options.force,
18576
+ keepBranchOnFailure: options.keepBranchOnFailure,
18577
+ isDefaultOwner: owner === options.defaultOwner,
18578
+ append: options.append,
18579
+ pr: options.pr,
18580
+ draftPr: options.draftPr,
18581
+ operationState: operationState || undefined,
18582
+ pathPattern: options.pathPattern,
18583
+ exclusive: options.exclusive,
18584
+ coOwned: options.coOwned
18585
+ });
18586
+ results.push(result);
18587
+ }
18588
+ const successCount = results.filter((r) => r.success).length;
18589
+ const failureCount = results.filter((r) => !r.success).length;
18590
+ log.header(options.append ? "Multi-branch update summary" : "Multi-branch creation summary");
18591
+ log.info(options.append ? `Successfully updated ${successCount} of ${codeowners2.length} branches` : `Successfully created ${successCount} of ${codeowners2.length} branches`);
18592
+ if (failureCount > 0) {
18593
+ log.error(`Failed: ${failureCount} branches`);
18594
+ }
18595
+ console.log("");
18596
+ const table = new import_cli_table33.default({
18597
+ head: ["Status", "Owner", "Branch", "Files", "Pushed", "PR"],
18598
+ colWidths: [10, 20, 40, 10, 10, 50],
18599
+ wordWrap: true
18600
+ });
18601
+ for (const result of results) {
18602
+ const status = result.success ? "✓" : "✗";
18603
+ const fileCount = result.files.length;
18604
+ const pushedStatus = result.pushed ? "✓" : "-";
18605
+ const prInfo = result.prUrl ? result.prUrl : result.error && result.error.includes("PR creation failed") ? "Failed" : "-";
18606
+ table.push([
18607
+ status,
18608
+ result.owner,
18609
+ result.branchName,
18610
+ `${fileCount} file${fileCount !== 1 ? "s" : ""}`,
18611
+ pushedStatus,
18612
+ prInfo
18613
+ ]);
18614
+ }
18615
+ console.log(table.toString());
18616
+ if (results.length > 0) {
18617
+ console.log(`
18618
+ Files by branch:`);
18619
+ for (const result of results) {
18620
+ if (result.success && result.files.length > 0) {
18621
+ console.log(`
18622
+ ${result.branchName} (${result.owner}):`);
18623
+ result.files.forEach((file) => console.log(` - ${file}`));
18624
+ }
18625
+ }
18626
+ }
18627
+ const errors2 = results.filter((r) => r.error);
18628
+ if (errors2.length > 0) {
18629
+ console.log(`
18630
+ Errors:`);
18631
+ errors2.forEach((result) => {
18632
+ console.log(`
18633
+ ${result.branchName} (${result.owner}):`);
18634
+ console.log(` ${result.error}`);
18635
+ });
18636
+ }
18637
+ if (operationState) {
18638
+ if (failureCount === 0) {
18639
+ completeOperation(operationState.id, true);
18640
+ } else {
18641
+ failOperation(operationState.id, `${failureCount} branch(es) failed`);
18642
+ log.info(`
18643
+ Note: ${failureCount} branch(es) failed. Files were auto-restored to working directory.`);
18644
+ log.info(`State preserved for reference. Run 'codeowners-git recover --id ${operationState.id}' if needed.`);
18645
+ }
18646
+ }
18647
+ } catch (err) {
18648
+ log.error(`Multi-branch operation failed: ${err}`);
18649
+ if (operationState) {
18650
+ log.info(`
18651
+ Attempting auto-recovery...`);
18652
+ const currentState = loadOperationState(operationState.id);
18653
+ if (currentState) {
18654
+ try {
18655
+ const recovered = await performRecovery(currentState, false);
18656
+ if (recovered) {
18657
+ log.success("Auto-recovery completed successfully");
18658
+ } else {
18659
+ log.warn("Auto-recovery completed with warnings");
18660
+ }
18661
+ } catch (recoveryError) {
18662
+ log.error(`Auto-recovery failed: ${recoveryError}`);
18663
+ log.info(`
18664
+ Manual recovery options:`);
18665
+ log.info(` 1. Run 'codeowners-git recover --id ${operationState.id}' to clean up`);
18666
+ log.info(` 2. Run 'codeowners-git recover --list' to see all operations`);
18667
+ }
18668
+ }
18669
+ }
18670
+ process.exit(1);
18671
+ }
18672
+ };
18673
+
18674
+ // src/commands/extract.ts
18675
+ var extract = async (options) => {
18676
+ try {
18677
+ if (!options.source) {
18678
+ log.error("Missing required option: --source");
18679
+ log.info(`
18680
+ Usage:`);
18681
+ log.info(" cg extract -s <branch-or-commit>");
18682
+ log.info(`
18683
+ Examples:`);
18684
+ log.info(" cg extract -s feature/other-branch");
18685
+ log.info(" cg extract -s feature/other-branch -i '@team-*'");
18686
+ log.info(" cg extract -s abc123def --compare-main");
18687
+ process.exit(1);
18688
+ }
18689
+ if (await hasUnstagedChanges()) {
18690
+ const unstagedFiles = await getUnstagedFiles();
18691
+ log.warn("Warning: Unstaged changes detected (these will be ignored):");
18692
+ unstagedFiles.forEach((file) => log.warn(` - ${file}`));
18693
+ log.info(`
18694
+ Only staged files will be processed.`);
18695
+ log.info("To stage files: git add <file>");
18696
+ log.info("");
18697
+ }
18698
+ log.info("Starting file extraction process...");
18699
+ let compareTarget;
18700
+ if (options.compareMain) {
18701
+ compareTarget = await getDefaultBranch();
18702
+ log.info(`Comparing ${options.source} against ${compareTarget}...`);
18703
+ } else {
18704
+ const baseBranch = await getBaseBranch(options.source);
18705
+ compareTarget = baseBranch;
18706
+ log.info(`Detected base branch: ${baseBranch}`);
18707
+ log.info(`Extracting changes from ${options.source}...`);
18708
+ }
18709
+ let changedFiles = await getChangedFilesBetween(options.source, compareTarget);
18710
+ if (changedFiles.length === 0) {
18711
+ log.warn(`No changed files found in ${options.source}`);
18712
+ return;
18713
+ }
18714
+ log.info(`Found ${changedFiles.length} changed file${changedFiles.length !== 1 ? "s" : ""}`);
18715
+ if (options.pathPattern) {
18716
+ changedFiles = filterByPathPatterns(changedFiles, options.pathPattern);
18717
+ log.info(`Filtered to ${changedFiles.length} file${changedFiles.length !== 1 ? "s" : ""} matching pattern: ${options.pathPattern}`);
18718
+ if (changedFiles.length === 0) {
18719
+ log.warn(`No files match the path pattern: ${options.pathPattern}`);
18720
+ return;
18721
+ }
18722
+ }
18723
+ let filesToExtract = changedFiles;
18724
+ if (options.coOwned && !options.include) {
18725
+ log.info("Filtering to co-owned files (2+ owners)");
18726
+ filesToExtract = changedFiles.filter((file) => {
18727
+ const owners = getOwner(file);
18728
+ return owners.length > 1;
18729
+ });
18730
+ if (filesToExtract.length === 0) {
18731
+ log.warn("No co-owned files found");
18732
+ return;
18733
+ }
18734
+ log.info(`Filtered to ${filesToExtract.length} co-owned file${filesToExtract.length !== 1 ? "s" : ""}`);
18735
+ }
18736
+ if (options.include) {
18737
+ log.info(`Filtering files by owner pattern: ${options.include}${options.exclusive ? " (exclusive)" : ""}${options.coOwned ? " (co-owned)" : ""}`);
18738
+ const ownedFiles = [];
18739
+ for (const file of filesToExtract) {
18740
+ const owners = getOwner(file);
18741
+ if (owners.length > 0) {
18742
+ if (options.coOwned && owners.length < 2) {
18743
+ continue;
18744
+ }
18745
+ let matches;
18746
+ if (options.exclusive) {
18747
+ matches = matchOwnersExclusive(owners, options.include);
18748
+ } else {
18749
+ matches = owners.some((owner) => matchOwnerPattern(owner, options.include));
18750
+ }
18751
+ if (matches) {
18752
+ ownedFiles.push(file);
18753
+ }
18754
+ }
18755
+ }
18756
+ filesToExtract = ownedFiles;
18757
+ if (filesToExtract.length === 0) {
18758
+ log.warn(`No files match the owner pattern: ${options.include}`);
18759
+ return;
18760
+ }
18761
+ log.info(`Filtered to ${filesToExtract.length} file${filesToExtract.length !== 1 ? "s" : ""}`);
18762
+ }
18763
+ log.info("Extracting files to working directory...");
18764
+ await extractFilesFromRef(options.source, filesToExtract);
18765
+ log.success(`
18766
+ ✓ Extracted ${filesToExtract.length} file${filesToExtract.length !== 1 ? "s" : ""} to working directory (unstaged)`);
18767
+ log.info(`
18768
+ Extracted files:`);
18769
+ filesToExtract.forEach((file) => log.info(` - ${file}`));
18770
+ log.info(`
18771
+ Next steps:`);
18772
+ log.info(" - Review the extracted files in your working directory");
18773
+ log.info(" - Use 'cg branch' command to create a branch and commit");
18774
+ log.info(" - Example: cg branch -i @my-team -b my-branch -m 'Commit message' -p");
18775
+ } catch (err) {
18776
+ log.error(`
18777
+ ✗ Extraction failed: ${err}`);
18778
+ process.exit(1);
18779
+ }
18780
+ };
18609
18781
  // package.json
18610
- var version = "1.8.0";
18782
+ var version = "2.0.0";
18611
18783
 
18612
18784
  // src/commands/version.ts
18613
18785
  function getVersion() {
@@ -18646,9 +18818,45 @@ To recover from incomplete operations, run:`);
18646
18818
  setupSignalHandlers();
18647
18819
  var program2 = new Command;
18648
18820
  program2.name("codeowners-git (cg)").description("CLI tool for grouping and managing staged files by CODEOWNERS").version(getVersion());
18649
- program2.command("list").description("Lists all git changed files by CODEOWNER").option("-o, --owner <owner>", "Filter by specific code owner").option("-i, --include <patterns>", "Filter by owner patterns").action(listCodeowners);
18650
- program2.command("branch").description("Create new branch with codeowner changes").requiredOption("-o, --owner <owner>", "Code owner name").requiredOption("-b, --branch <branch>", "Branch name").requiredOption("-m, --message <message>", "Commit message").option("-n, --no-verify", "Skip lint-staged or any other ci checks").option("-p, --push", "Push branch to remote after commit").option("-r, --remote <remote>", "Remote name to push to", "origin").option("-u, --upstream <upstream>", "Upstream branch name (defaults to local branch name)").option("-f, --force", "Force push to remote").option("-k, --keep-branch-on-failure", "Keep the created branch even if operation fails").option("--append", "Add commits to existing branch instead of creating a new one").option("--pr", "Create a pull request after pushing (requires --push)").option("--draft-pr", "Create a draft pull request after pushing (requires --push)").action(branch);
18651
- program2.command("multi-branch").description("Create branches for all codeowners").requiredOption("-b, --branch <branch>", "Base branch name (will be suffixed with codeowner name)").requiredOption("-m, --message <message>", "Base commit message (will be suffixed with codeowner name)").option("-n, --no-verify", "Skip lint-staged or any other ci checks").option("-p, --push", "Push branches to remote after commit").option("-r, --remote <remote>", "Remote name to push to", "origin").option("-u, --upstream <upstream>", "Upstream branch name pattern (defaults to local branch name)").option("-f, --force", "Force push to remote").option("-k, --keep-branch-on-failure", "Keep created branches even if operation fails").option("-d, --default-owner <defaultOwner>", "Default owner to use when no codeowners are found for changed files").option("--ignore <patterns>", "Comma-separated patterns to exclude codeowners (e.g., 'team-a,team-b')").option("--include <patterns>", "Comma-separated patterns to include codeowners (e.g., 'team-*,@org/*')").option("--append", "Add commits to existing branches instead of creating new ones").option("--pr", "Create pull requests after pushing (requires --push)").option("--draft-pr", "Create draft pull requests after pushing (requires --push)").action(multiBranch);
18652
- program2.command("extract").description("Extract file changes from a branch or commit to working directory").requiredOption("-s, --source <source>", "Source branch or commit to extract from").option("-o, --owner <owner>", "Filter extracted files by code owner (supports micromatch patterns)").option("--compare-main", "Compare source against main branch instead of detecting merge-base").action(extract);
18821
+ program2.command("list").description("Lists all git changed files by CODEOWNER").argument("[pattern]", "Path pattern to filter files (micromatch syntax, comma-separated)").option("-i, --include <patterns>", "Filter by owner patterns").option("-g, --group", "Group files by code owner").option("-e, --exclusive", "Only include files where the owner is the sole owner (no co-owners)").option("-c, --co-owned", "Only include files with multiple owners (co-owned files)").action((pattern, options) => {
18822
+ if (options.exclusive && options.coOwned) {
18823
+ console.error("Error: Cannot use both --exclusive and --co-owned options");
18824
+ process.exit(1);
18825
+ }
18826
+ listCodeowners({
18827
+ ...options,
18828
+ pathPattern: pattern
18829
+ });
18830
+ });
18831
+ program2.command("branch").description("Create new branch with codeowner changes").argument("[pattern]", "Path pattern to filter files (micromatch syntax, comma-separated)").requiredOption("-i, --include <patterns>", "Code owner pattern to filter files").requiredOption("-b, --branch <branch>", "Branch name").requiredOption("-m, --message <message>", "Commit message").option("-n, --no-verify", "Skip lint-staged or any other ci checks").option("-p, --push", "Push branch to remote after commit").option("-r, --remote <remote>", "Remote name to push to", "origin").option("-u, --upstream <upstream>", "Upstream branch name (defaults to local branch name)").option("-f, --force", "Force push to remote").option("-k, --keep-branch-on-failure", "Keep the created branch even if operation fails").option("--append", "Add commits to existing branch instead of creating a new one").option("--pr", "Create a pull request after pushing (requires --push)").option("--draft-pr", "Create a draft pull request after pushing (requires --push)").option("-e, --exclusive", "Only include files where the owner is the sole owner (no co-owners)").option("-c, --co-owned", "Only include files with multiple owners (co-owned files)").action((pattern, options) => {
18832
+ if (options.exclusive && options.coOwned) {
18833
+ console.error("Error: Cannot use both --exclusive and --co-owned options");
18834
+ process.exit(1);
18835
+ }
18836
+ branch({
18837
+ ...options,
18838
+ pathPattern: pattern
18839
+ });
18840
+ });
18841
+ program2.command("multi-branch").description("Create branches for all codeowners").argument("[pattern]", "Path pattern to filter files (micromatch syntax, comma-separated)").requiredOption("-b, --branch <branch>", "Base branch name (will be suffixed with codeowner name)").requiredOption("-m, --message <message>", "Base commit message (will be suffixed with codeowner name)").option("-n, --no-verify", "Skip lint-staged or any other ci checks").option("-p, --push", "Push branches to remote after commit").option("-r, --remote <remote>", "Remote name to push to", "origin").option("-u, --upstream <upstream>", "Upstream branch name pattern (defaults to local branch name)").option("-f, --force", "Force push to remote").option("-k, --keep-branch-on-failure", "Keep created branches even if operation fails").option("-d, --default-owner <defaultOwner>", "Default owner to use when no codeowners are found for changed files").option("--ignore <patterns>", "Comma-separated patterns to exclude codeowners (e.g., 'team-a,team-b')").option("--include <patterns>", "Comma-separated patterns to include codeowners (e.g., 'team-*,@org/*')").option("--append", "Add commits to existing branches instead of creating new ones").option("--pr", "Create pull requests after pushing (requires --push)").option("--draft-pr", "Create draft pull requests after pushing (requires --push)").option("-e, --exclusive", "Only include files where each owner is the sole owner (no co-owners)").option("-c, --co-owned", "Only include files with multiple owners (co-owned files)").action((pattern, options) => {
18842
+ if (options.exclusive && options.coOwned) {
18843
+ console.error("Error: Cannot use both --exclusive and --co-owned options");
18844
+ process.exit(1);
18845
+ }
18846
+ multiBranch({
18847
+ ...options,
18848
+ pathPattern: pattern
18849
+ });
18850
+ });
18851
+ program2.command("extract").description("Extract file changes from a branch or commit to working directory").argument("[pattern]", "Path pattern to filter files (micromatch syntax, comma-separated)").requiredOption("-s, --source <source>", "Source branch or commit to extract from").option("-i, --include <patterns>", "Filter extracted files by code owner pattern").option("--compare-main", "Compare source against main branch instead of detecting merge-base").option("-e, --exclusive", "Only include files where the owner is the sole owner (no co-owners)").option("-c, --co-owned", "Only include files with multiple owners (co-owned files)").action((pattern, options) => {
18852
+ if (options.exclusive && options.coOwned) {
18853
+ console.error("Error: Cannot use both --exclusive and --co-owned options");
18854
+ process.exit(1);
18855
+ }
18856
+ extract({
18857
+ ...options,
18858
+ pathPattern: pattern
18859
+ });
18860
+ });
18653
18861
  program2.command("recover").description("Recover from failed or incomplete operations").option("--id <operationId>", "Specific operation ID to recover").option("--keep-branches", "Keep created branches instead of deleting them").option("--list", "List all incomplete operations").option("--auto", "Automatically recover most recent operation without prompts").action(recover);
18654
18862
  program2.parse(process.argv);