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.
- package/README.md +51 -1
- package/dist/cli.js +212 -20
- 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
|
|
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
|
-
|
|
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
|
-
var commitChanges = async (files, message) => {
|
|
14618
|
-
|
|
14619
|
-
|
|
14620
|
-
|
|
14621
|
-
|
|
14622
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14721
|
-
|
|
14722
|
-
|
|
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);
|