codeowners-git 1.1.0 → 1.2.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 +43 -3
  2. package/dist/cli.js +207 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -14,8 +14,6 @@ Managing large-scale migrations in big monorepos with multiple codeowners can be
14
14
 
15
15
  https://github.com/user-attachments/assets/7cc0a924-f03e-47f3-baad-63eca9e8e4a8
16
16
 
17
-
18
-
19
17
  ## Installation
20
18
 
21
19
  ### Using npx (recommended)
@@ -85,13 +83,54 @@ Options:
85
83
  - `--branch, -b` Specify branch pattern
86
84
  - `--message, -m` Commit message for changes
87
85
  - `--no-verify, -n` Skips lint-staged and other checks before committing
86
+ - `--push, -p` Push branch to remote after commit
87
+ - `--remote, -r` Remote name to push to (default: "origin")
88
+ - `--upstream, -u` Upstream branch name (defaults to local branch name)
89
+ - `--force, -f` Force push to remote
90
+ - `--keep-branch-on-failure, -k` Keep the created branch even if operation fails
88
91
 
89
92
  Example:
90
93
 
91
94
  ```bash
92
- codeowners-git branch -o @myteam -b "feature/*" -m "Add feature branch ownership"
95
+ codeowners-git branch -o @myteam -b "feature/new-feature" -m "Add new feature" -p
93
96
  ```
94
97
 
98
+ ### `multi-branch`
99
+
100
+ Create branches for all codeowners with changes.
101
+
102
+ Usage:
103
+
104
+ ```bash
105
+ codeowners-git multi-branch [options]
106
+ ```
107
+
108
+ Options:
109
+
110
+ - `--branch, -b` Base branch name (will be prefixed with codeowner name)
111
+ - `--message, -m` Base commit message (will be suffixed with codeowner name)
112
+ - `--no-verify, -n` Skips lint-staged and other checks before committing
113
+ - `--push, -p` Push branches to remote after commit
114
+ - `--remote, -r` Remote name to push to (default: "origin")
115
+ - `--upstream, -u` Upstream branch name pattern (defaults to local branch name)
116
+ - `--force, -f` Force push to remote
117
+ - `--keep-branch-on-failure, -k` Keep created branches even if operation fails
118
+
119
+ Example:
120
+
121
+ ```bash
122
+ codeowners-git multi-branch -b "feature/new-feature" -m "Add new feature" -p
123
+ ```
124
+
125
+ This will:
126
+
127
+ 1. Find all codeowners for the changed files in your repository
128
+ 2. For each codeowner (e.g., @team-a, @team-b):
129
+ - Create a branch like `team-a/feature/new-feature`
130
+ - Commit only the files owned by that team
131
+ - Add a commit message like "Add new feature - @team-a"
132
+ - Push each branch to the remote if the `-p` flag is provided
133
+
95
134
  ## Contributing
96
135
 
97
136
  1. Clone the repository
@@ -111,6 +150,7 @@ bun test
111
150
  5. Submit a pull request
112
151
 
113
152
  ## Alternatives
153
+
114
154
  [@snyk/github-codeowners](https://github.com/snyk/github-codeowners)
115
155
 
116
156
  [codeowners](https://github.com/beaugunderson/codeowners)
package/dist/cli.js CHANGED
@@ -14600,32 +14600,109 @@ ${message}`)),
14600
14600
  };
14601
14601
 
14602
14602
  // src/utils/git.ts
14603
+ import { spawn as spawn2 } from "child_process";
14603
14604
  var git = esm_default();
14604
14605
  var getChangedFiles = async () => {
14605
14606
  const status = await git.status();
14606
14607
  return status.files.map((file) => file.path);
14607
14608
  };
14609
+ var branchExists = async (branchName) => {
14610
+ try {
14611
+ const branches = await git.branch();
14612
+ return branches.all.includes(branchName);
14613
+ } catch (error) {
14614
+ log.error(`Failed to check if branch exists: ${error}`);
14615
+ return false;
14616
+ }
14617
+ };
14608
14618
  var createBranch = async (branchName) => {
14609
14619
  log.info(`Switching to a new local branch: "${branchName}"`);
14610
- await git.checkoutLocalBranch(branchName);
14611
- log.info(`Now on branch: "${branchName}"`);
14620
+ try {
14621
+ await git.checkoutLocalBranch(branchName);
14622
+ log.info(`Now on branch: "${branchName}"`);
14623
+ } catch (error) {
14624
+ throw new Error(`Failed to create branch "${branchName}": ${error}`);
14625
+ }
14626
+ };
14627
+ var deleteBranch = async (branchName, force = false) => {
14628
+ try {
14629
+ if (await branchExists(branchName)) {
14630
+ log.info(`Deleting branch: "${branchName}"${force ? " (forced)" : ""}`);
14631
+ await git.branch([force ? "-D" : "-d", branchName]);
14632
+ log.info(`Branch "${branchName}" deleted successfully`);
14633
+ return true;
14634
+ }
14635
+ return false;
14636
+ } catch (error) {
14637
+ log.error(`Failed to delete branch "${branchName}": ${error}`);
14638
+ return false;
14639
+ }
14612
14640
  };
14613
14641
  var checkout = async (name) => {
14614
14642
  log.info(`Switching to branch: "${name}"`);
14615
- await git.checkout(name);
14643
+ try {
14644
+ await git.checkout(name);
14645
+ } catch (error) {
14646
+ throw new Error(`Failed to checkout branch "${name}": ${error}`);
14647
+ }
14616
14648
  };
14617
14649
  var commitChanges = async (files, { message, noVerify = false }) => {
14618
- log.info("Adding files to commit...");
14619
- await git.add(files);
14620
- const commitOptions = noVerify ? ["--no-verify"] : [];
14621
- log.info(`Running commit with message: "${message}"`);
14622
- await git.commit(message, [], {
14623
- ...noVerify ? { "--no-verify": null } : {}
14624
- });
14625
- log.info("Commit finished successfully.");
14650
+ try {
14651
+ log.info("Adding files to commit...");
14652
+ await git.add(files);
14653
+ log.info(`Running commit with message: "${message}"`);
14654
+ await git.commit(message, [], {
14655
+ ...noVerify ? { "--no-verify": null } : {}
14656
+ });
14657
+ log.info("Commit finished successfully.");
14658
+ } catch (error) {
14659
+ log.error(`Failed to commit changes: ${error}`);
14660
+ throw new Error(`Commit failed: ${error}`);
14661
+ }
14626
14662
  };
14627
14663
  var getCurrentBranch = async () => {
14628
- return await git.revparse(["--abbrev-ref", "HEAD"]);
14664
+ try {
14665
+ return await git.revparse(["--abbrev-ref", "HEAD"]);
14666
+ } catch (error) {
14667
+ throw new Error(`Failed to get current branch: ${error}`);
14668
+ }
14669
+ };
14670
+ var pushBranch = async (branchName, {
14671
+ remote = "origin",
14672
+ upstream,
14673
+ force = false,
14674
+ noVerify = false
14675
+ } = {}) => {
14676
+ const targetUpstream = upstream || branchName;
14677
+ log.info(`Pushing branch "${branchName}" to ${remote}/${targetUpstream}...`);
14678
+ try {
14679
+ const pushArgs = [];
14680
+ pushArgs.push(remote, `${branchName}:${targetUpstream}`);
14681
+ if (force) {
14682
+ pushArgs.push("--force");
14683
+ }
14684
+ if (noVerify) {
14685
+ pushArgs.push("--no-verify");
14686
+ }
14687
+ const gitProcess = spawn2("git", ["push", ...pushArgs], {
14688
+ stdio: ["inherit", "inherit", "inherit"]
14689
+ });
14690
+ return new Promise((resolve, reject) => {
14691
+ gitProcess.on("close", (code) => {
14692
+ if (code === 0) {
14693
+ log.success(`Successfully pushed to ${remote}/${targetUpstream}`);
14694
+ resolve();
14695
+ } else {
14696
+ const error = new Error(`Push failed with exit code ${code}`);
14697
+ log.error(`Failed to push to remote: ${error}`);
14698
+ reject(error);
14699
+ }
14700
+ });
14701
+ });
14702
+ } catch (error) {
14703
+ log.error(`Failed to push to remote: ${error}`);
14704
+ throw new Error(`Push failed: ${error}`);
14705
+ }
14629
14706
  };
14630
14707
 
14631
14708
  // src/utils/codeowners.ts
@@ -14703,35 +14780,143 @@ var listCodeowners = async (options) => {
14703
14780
 
14704
14781
  // src/commands/branch.ts
14705
14782
  var branch = async (options) => {
14783
+ let originalBranch = "";
14784
+ let stashId = null;
14785
+ let newBranchCreated = false;
14786
+ let filesToCommit = [];
14706
14787
  try {
14707
14788
  if (!options.branch || !options.message || !options.owner) {
14708
14789
  throw new Error("Missing required options for branch creation");
14709
14790
  }
14710
- const filesToCommit = await getOwnerFiles(options.owner);
14791
+ log.info("Starting branch creation process...");
14792
+ originalBranch = await getCurrentBranch();
14793
+ log.info(`Currently on branch: ${originalBranch}`);
14794
+ filesToCommit = await getOwnerFiles(options.owner);
14711
14795
  if (filesToCommit.length <= 0) {
14712
14796
  throw new Error(`No files found for ${options.owner}`);
14713
14797
  }
14714
- log.info("Starting branch creation process...");
14715
- const originalBranch = await getCurrentBranch();
14716
- log.info(`Currently on branch: ${originalBranch}`);
14717
14798
  log.file(`Files to be committed:
14718
14799
  ${filesToCommit.join(`
14719
14800
  `)}`);
14801
+ if (await branchExists(options.branch)) {
14802
+ throw new Error(`Branch "${options.branch}" already exists. Use a different name or delete the existing branch first.`);
14803
+ }
14720
14804
  try {
14721
14805
  log.info(`Creating new branch "${options.branch}"...`);
14722
14806
  await createBranch(options.branch);
14723
- log.info(`Committing changes with message: "${options.message}" ${!options.verify}...`);
14807
+ newBranchCreated = true;
14808
+ log.info(`Committing changes with message: "${options.message}" ${!options.verify ? "(no-verify)" : ""}...`);
14724
14809
  await commitChanges(filesToCommit, {
14725
14810
  message: options.message ?? "",
14726
14811
  noVerify: !options.verify
14727
14812
  });
14728
- } finally {
14813
+ if (options.push) {
14814
+ await pushBranch(options.branch, {
14815
+ remote: options.remote,
14816
+ upstream: options.upstream,
14817
+ force: options.force,
14818
+ noVerify: !options.verify
14819
+ });
14820
+ }
14729
14821
  log.info(`Checking out original branch "${originalBranch}"...`);
14730
14822
  await checkout(originalBranch);
14731
- log.success(`Branch "${options.branch}" created and changes committed.`);
14823
+ log.success(options.push ? `Branch "${options.branch}" created, changes committed, and pushed to remote.` : `Branch "${options.branch}" created and changes committed.`);
14824
+ } catch (operationError) {
14825
+ log.error(`Operation failed: ${operationError}`);
14826
+ if (newBranchCreated) {
14827
+ try {
14828
+ log.info(`Returning to original branch "${originalBranch}"...`);
14829
+ await checkout(originalBranch);
14830
+ if (!options.keepBranchOnFailure) {
14831
+ log.info(`Cleaning up: Deleting branch "${options.branch}"...`);
14832
+ await deleteBranch(options.branch, true);
14833
+ } else {
14834
+ log.info(`Branch "${options.branch}" was kept despite the failure.`);
14835
+ }
14836
+ } catch (cleanupError) {
14837
+ log.error(`Error during cleanup: ${cleanupError}`);
14838
+ }
14839
+ }
14840
+ throw operationError;
14732
14841
  }
14733
14842
  } catch (err) {
14734
- log.error(err);
14843
+ log.error(`Branch operation failed: ${err}`);
14844
+ process.exit(1);
14845
+ } finally {
14846
+ try {
14847
+ if (originalBranch) {
14848
+ const currentBranch = await getCurrentBranch();
14849
+ if (currentBranch !== originalBranch) {
14850
+ await checkout(originalBranch);
14851
+ }
14852
+ }
14853
+ } catch (finalError) {
14854
+ log.error(`Error during final cleanup: ${finalError}`);
14855
+ log.info("Some manual cleanup may be required.");
14856
+ }
14857
+ }
14858
+ };
14859
+
14860
+ // src/commands/multi-branch.ts
14861
+ var multiBranch = async (options) => {
14862
+ try {
14863
+ if (!options.branch || !options.message) {
14864
+ throw new Error("Missing required options for multi-branch creation");
14865
+ }
14866
+ log.info("Starting multi-branch creation process...");
14867
+ const changedFiles = await getChangedFiles();
14868
+ if (changedFiles.length === 0) {
14869
+ throw new Error("No changed files found in the repository");
14870
+ }
14871
+ const ownerSet = new Set;
14872
+ for (const file of changedFiles) {
14873
+ const owners = getOwner(file);
14874
+ for (const owner of owners) {
14875
+ ownerSet.add(owner);
14876
+ }
14877
+ }
14878
+ const codeowners2 = Array.from(ownerSet);
14879
+ if (codeowners2.length === 0) {
14880
+ throw new Error("No codeowners found for the changed files");
14881
+ }
14882
+ log.info(`Found ${codeowners2.length} codeowners: ${codeowners2.join(", ")}`);
14883
+ const results = {
14884
+ success: [],
14885
+ failure: []
14886
+ };
14887
+ for (const owner of codeowners2) {
14888
+ try {
14889
+ const sanitizedOwner = owner.replace(/[^a-zA-Z0-9-_@]/g, "-").replace(/^@/, "");
14890
+ const branchName = `${options.branch}/${sanitizedOwner}`;
14891
+ const commitMessage = `${options.message} - ${owner}`;
14892
+ log.info(`Creating branch for ${owner}...`);
14893
+ await branch({
14894
+ owner,
14895
+ branch: branchName,
14896
+ message: commitMessage,
14897
+ verify: options.verify,
14898
+ push: options.push,
14899
+ remote: options.remote,
14900
+ upstream: options.upstream,
14901
+ force: options.force,
14902
+ keepBranchOnFailure: options.keepBranchOnFailure
14903
+ });
14904
+ results.success.push(owner);
14905
+ } catch (error) {
14906
+ log.error(`Failed to create branch for ${owner}: ${error}`);
14907
+ results.failure.push(owner);
14908
+ }
14909
+ }
14910
+ log.header("Multi-branch creation summary");
14911
+ log.info(`Successfully created branches for ${results.success.length} of ${codeowners2.length} codeowners`);
14912
+ if (results.success.length) {
14913
+ log.success(`Successful: ${results.success.join(", ")}`);
14914
+ }
14915
+ if (results.failure.length) {
14916
+ log.error(`Failed: ${results.failure.join(", ")}`);
14917
+ }
14918
+ } catch (err) {
14919
+ log.error(`Multi-branch operation failed: ${err}`);
14735
14920
  process.exit(1);
14736
14921
  }
14737
14922
  };
@@ -14740,5 +14925,6 @@ var branch = async (options) => {
14740
14925
  var program2 = new Command;
14741
14926
  program2.name("codeowners").description("CLI tool for grouping and managing staged files by CODEOWNERS");
14742
14927
  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);
14743
- 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").action(branch);
14928
+ 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").action(branch);
14929
+ 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").action(multiBranch);
14744
14930
  program2.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeowners-git",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "module": "src/cli.ts",
5
5
  "type": "module",
6
6
  "private": false,