codeowners-git 1.0.5 → 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 +51 -1
  2. package/dist/cli.js +212 -20
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -12,6 +12,8 @@ Managing large-scale migrations in big monorepos with multiple codeowners can be
12
12
  - Creating compact, team-specific branches with only their affected files.
13
13
  - Streamlining the review process with smaller, targeted PRs.
14
14
 
15
+ https://github.com/user-attachments/assets/7cc0a924-f03e-47f3-baad-63eca9e8e4a8
16
+
15
17
  ## Installation
16
18
 
17
19
  ### Using npx (recommended)
@@ -80,13 +82,55 @@ Options:
80
82
  - `--owner, -o` Specify owner(s) to add/remove
81
83
  - `--branch, -b` Specify branch pattern
82
84
  - `--message, -m` Commit message for changes
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
83
91
 
84
92
  Example:
85
93
 
86
94
  ```bash
87
- 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
88
96
  ```
89
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
+
90
134
  ## Contributing
91
135
 
92
136
  1. Clone the repository
@@ -105,6 +149,12 @@ bun test
105
149
 
106
150
  5. Submit a pull request
107
151
 
152
+ ## Alternatives
153
+
154
+ [@snyk/github-codeowners](https://github.com/snyk/github-codeowners)
155
+
156
+ [codeowners](https://github.com/beaugunderson/codeowners)
157
+
108
158
  ## License
109
159
 
110
160
  MIT ©
package/dist/cli.js CHANGED
@@ -14600,29 +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
- var commitChanges = async (files, message) => {
14618
- log.info("Adding files to commit...");
14619
- await git.add(files);
14620
- log.info(`Running commit with message: "${message}"`);
14621
- await git.commit(message);
14622
- log.info("Commit finished successfully.");
14649
+ var commitChanges = async (files, { message, noVerify = false }) => {
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
+ }
14623
14662
  };
14624
14663
  var getCurrentBranch = async () => {
14625
- 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
+ }
14626
14706
  };
14627
14707
 
14628
14708
  // src/utils/codeowners.ts
@@ -14700,32 +14780,143 @@ var listCodeowners = async (options) => {
14700
14780
 
14701
14781
  // src/commands/branch.ts
14702
14782
  var branch = async (options) => {
14783
+ let originalBranch = "";
14784
+ let stashId = null;
14785
+ let newBranchCreated = false;
14786
+ let filesToCommit = [];
14703
14787
  try {
14704
14788
  if (!options.branch || !options.message || !options.owner) {
14705
14789
  throw new Error("Missing required options for branch creation");
14706
14790
  }
14707
- 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);
14708
14795
  if (filesToCommit.length <= 0) {
14709
14796
  throw new Error(`No files found for ${options.owner}`);
14710
14797
  }
14711
- log.info("Starting branch creation process...");
14712
- const originalBranch = await getCurrentBranch();
14713
- log.info(`Currently on branch: ${originalBranch}`);
14714
14798
  log.file(`Files to be committed:
14715
14799
  ${filesToCommit.join(`
14716
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
+ }
14717
14804
  try {
14718
14805
  log.info(`Creating new branch "${options.branch}"...`);
14719
14806
  await createBranch(options.branch);
14720
- log.info(`Committing changes with message: "${options.message}"...`);
14721
- await commitChanges(filesToCommit, options.message);
14722
- } finally {
14807
+ newBranchCreated = true;
14808
+ log.info(`Committing changes with message: "${options.message}" ${!options.verify ? "(no-verify)" : ""}...`);
14809
+ await commitChanges(filesToCommit, {
14810
+ message: options.message ?? "",
14811
+ noVerify: !options.verify
14812
+ });
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
+ }
14723
14821
  log.info(`Checking out original branch "${originalBranch}"...`);
14724
14822
  await checkout(originalBranch);
14725
- 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;
14726
14841
  }
14727
14842
  } catch (err) {
14728
- 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}`);
14729
14920
  process.exit(1);
14730
14921
  }
14731
14922
  };
@@ -14734,5 +14925,6 @@ var branch = async (options) => {
14734
14925
  var program2 = new Command;
14735
14926
  program2.name("codeowners").description("CLI tool for grouping and managing staged files by CODEOWNERS");
14736
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);
14737
- 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").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);
14738
14930
  program2.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeowners-git",
3
- "version": "1.0.5",
3
+ "version": "1.2.0",
4
4
  "module": "src/cli.ts",
5
5
  "type": "module",
6
6
  "private": false,