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.
- package/README.md +43 -3
- package/dist/cli.js +207 -21
- 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
|
|
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
|
-
|
|
14611
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14619
|
-
|
|
14620
|
-
|
|
14621
|
-
|
|
14622
|
-
|
|
14623
|
-
|
|
14624
|
-
|
|
14625
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|