contribute-now 0.4.1 → 0.5.0-dev.9ad3d01
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 +39 -12
- package/dist/index.js +414 -46
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -204,17 +204,19 @@ Checks include:
|
|
|
204
204
|
|
|
205
205
|
### `contrib log`
|
|
206
206
|
|
|
207
|
-
Show a colorized, workflow-aware commit log
|
|
207
|
+
Show a colorized, workflow-aware commit log. By default it shows only **local unpushed commits** — the changes you've made since the last push (or since branching off the base branch). Use flags to switch between different views.
|
|
208
208
|
|
|
209
209
|
```bash
|
|
210
|
-
contrib log #
|
|
211
|
-
contrib log
|
|
212
|
-
contrib log --
|
|
213
|
-
contrib log --
|
|
210
|
+
contrib log # local unpushed commits (default)
|
|
211
|
+
contrib log --remote # commits on remote not yet pulled
|
|
212
|
+
contrib log --full # full history for the current branch
|
|
213
|
+
contrib log --all # commits across all branches
|
|
214
|
+
contrib log -n 50 # change the commit limit (default: 20)
|
|
214
215
|
contrib log -b feature/x # log for a specific branch
|
|
216
|
+
contrib log --no-graph # flat view without graph lines
|
|
215
217
|
```
|
|
216
218
|
|
|
217
|
-
|
|
219
|
+
When no upstream tracking is set (branch hasn't been pushed yet), the command automatically compares against the base branch from your config (e.g., `origin/dev`). Protected branches are highlighted, and the current branch is color-coded for quick orientation.
|
|
218
220
|
|
|
219
221
|
---
|
|
220
222
|
|
|
@@ -352,15 +354,40 @@ bun test # run tests
|
|
|
352
354
|
bun run lint # check code quality
|
|
353
355
|
```
|
|
354
356
|
|
|
355
|
-
## Contributing
|
|
357
|
+
## 🎯 Contributing
|
|
356
358
|
|
|
357
|
-
Contributions are welcome
|
|
359
|
+
Contributions are welcome, create a pull request to this repo and I will review your code. Please consider to submit your pull request to the `dev` branch. Thank you!
|
|
358
360
|
|
|
359
|
-
|
|
361
|
+
Read the project's [contributing guide](./CONTRIBUTING.md) for more info.
|
|
360
362
|
|
|
361
|
-
|
|
363
|
+
## 🐛 Issues
|
|
362
364
|
|
|
363
|
-
|
|
365
|
+
Please report any issues and bugs by [creating a new issue here](https://github.com/warengonzaga/contribute-now/issues/new/choose), also make sure you're reporting an issue that doesn't exist. Any help to improve the project would be appreciated. Thanks! 🙏✨
|
|
366
|
+
|
|
367
|
+
## 🙏 Sponsor
|
|
368
|
+
|
|
369
|
+
Like this project? **Leave a star**! ⭐⭐⭐⭐⭐
|
|
370
|
+
|
|
371
|
+
Want to support my work and get some perks? [Become a sponsor](https://github.com/sponsors/warengonzaga)! 💖
|
|
372
|
+
|
|
373
|
+
Or, you just love what I do? [Buy me a coffee](https://buymeacoffee.com/warengonzaga)! ☕
|
|
374
|
+
|
|
375
|
+
Recognized my open-source contributions? [Nominate me](https://stars.github.com/nominate) as GitHub Star! 💫
|
|
364
376
|
|
|
365
|
-
|
|
377
|
+
## 📋 Code of Conduct
|
|
378
|
+
|
|
379
|
+
Read the project's [code of conduct](./CODE_OF_CONDUCT.md).
|
|
380
|
+
|
|
381
|
+
## 📃 License
|
|
382
|
+
|
|
383
|
+
This project is licensed under [GNU General Public License v3.0](https://opensource.org/licenses/GPL-3.0).
|
|
384
|
+
|
|
385
|
+
## 📝 Author
|
|
386
|
+
|
|
387
|
+
This project is created by **[Waren Gonzaga](https://github.com/warengonzaga)**, with the help of awesome [contributors](https://github.com/warengonzaga/contribute-now/graphs/contributors).
|
|
388
|
+
|
|
389
|
+
[](https://github.com/warengonzaga/contribute-now/graphs/contributors)
|
|
390
|
+
|
|
391
|
+
---
|
|
366
392
|
|
|
393
|
+
💻💖☕ by [Waren Gonzaga](https://warengonzaga.com) & [YHWH](https://www.youtube.com/watch?v=VOZbswniA-g) 🙏 — Without *Him*, none of this exists, *even me*.
|
package/dist/index.js
CHANGED
|
@@ -547,6 +547,76 @@ async function getLogEntries(options) {
|
|
|
547
547
|
return { hash: hash.trim(), subject: subject.trim(), refs: refs.trim() };
|
|
548
548
|
});
|
|
549
549
|
}
|
|
550
|
+
async function getLocalCommitsGraph(options) {
|
|
551
|
+
const count = options?.count ?? 20;
|
|
552
|
+
const upstream = options?.upstream;
|
|
553
|
+
if (!upstream)
|
|
554
|
+
return [];
|
|
555
|
+
const args = [
|
|
556
|
+
"log",
|
|
557
|
+
"--oneline",
|
|
558
|
+
"--graph",
|
|
559
|
+
"--decorate",
|
|
560
|
+
`--max-count=${count}`,
|
|
561
|
+
"--color=never",
|
|
562
|
+
`${upstream}..HEAD`
|
|
563
|
+
];
|
|
564
|
+
const { exitCode, stdout } = await run(args);
|
|
565
|
+
if (exitCode !== 0)
|
|
566
|
+
return [];
|
|
567
|
+
return stdout.trimEnd().split(`
|
|
568
|
+
`).filter(Boolean);
|
|
569
|
+
}
|
|
570
|
+
async function getLocalCommitsEntries(options) {
|
|
571
|
+
const count = options?.count ?? 20;
|
|
572
|
+
const upstream = options?.upstream;
|
|
573
|
+
if (!upstream)
|
|
574
|
+
return [];
|
|
575
|
+
const args = ["log", `--format=%h||%s||%D`, `--max-count=${count}`, `${upstream}..HEAD`];
|
|
576
|
+
const { exitCode, stdout } = await run(args);
|
|
577
|
+
if (exitCode !== 0)
|
|
578
|
+
return [];
|
|
579
|
+
return stdout.trimEnd().split(`
|
|
580
|
+
`).filter(Boolean).map((line) => {
|
|
581
|
+
const [hash = "", subject = "", refs = ""] = line.split("||");
|
|
582
|
+
return { hash: hash.trim(), subject: subject.trim(), refs: refs.trim() };
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
async function getRemoteOnlyCommitsGraph(options) {
|
|
586
|
+
const count = options?.count ?? 20;
|
|
587
|
+
const upstream = options?.upstream;
|
|
588
|
+
if (!upstream)
|
|
589
|
+
return [];
|
|
590
|
+
const args = [
|
|
591
|
+
"log",
|
|
592
|
+
"--oneline",
|
|
593
|
+
"--graph",
|
|
594
|
+
"--decorate",
|
|
595
|
+
`--max-count=${count}`,
|
|
596
|
+
"--color=never",
|
|
597
|
+
`HEAD..${upstream}`
|
|
598
|
+
];
|
|
599
|
+
const { exitCode, stdout } = await run(args);
|
|
600
|
+
if (exitCode !== 0)
|
|
601
|
+
return [];
|
|
602
|
+
return stdout.trimEnd().split(`
|
|
603
|
+
`).filter(Boolean);
|
|
604
|
+
}
|
|
605
|
+
async function getRemoteOnlyCommitsEntries(options) {
|
|
606
|
+
const count = options?.count ?? 20;
|
|
607
|
+
const upstream = options?.upstream;
|
|
608
|
+
if (!upstream)
|
|
609
|
+
return [];
|
|
610
|
+
const args = ["log", `--format=%h||%s||%D`, `--max-count=${count}`, `HEAD..${upstream}`];
|
|
611
|
+
const { exitCode, stdout } = await run(args);
|
|
612
|
+
if (exitCode !== 0)
|
|
613
|
+
return [];
|
|
614
|
+
return stdout.trimEnd().split(`
|
|
615
|
+
`).filter(Boolean).map((line) => {
|
|
616
|
+
const [hash = "", subject = "", refs = ""] = line.split("||");
|
|
617
|
+
return { hash: hash.trim(), subject: subject.trim(), refs: refs.trim() };
|
|
618
|
+
});
|
|
619
|
+
}
|
|
550
620
|
async function getLocalBranches() {
|
|
551
621
|
const { exitCode, stdout } = await run(["branch", "-vv", "--no-color"]);
|
|
552
622
|
if (exitCode !== 0)
|
|
@@ -575,6 +645,17 @@ async function getRemoteBranches() {
|
|
|
575
645
|
return stdout.trimEnd().split(`
|
|
576
646
|
`).map((line) => line.trim()).filter((line) => line.length > 0 && !line.includes(" -> "));
|
|
577
647
|
}
|
|
648
|
+
async function isBranchMergedInto(branch, base) {
|
|
649
|
+
const { exitCode } = await run(["merge-base", "--is-ancestor", branch, base]);
|
|
650
|
+
return exitCode === 0;
|
|
651
|
+
}
|
|
652
|
+
async function getLastCommitDate(branch) {
|
|
653
|
+
const { exitCode, stdout } = await run(["log", "-1", "--format=%aI", branch]);
|
|
654
|
+
if (exitCode !== 0)
|
|
655
|
+
return null;
|
|
656
|
+
const date = stdout.trim();
|
|
657
|
+
return date || null;
|
|
658
|
+
}
|
|
578
659
|
|
|
579
660
|
// src/utils/logger.ts
|
|
580
661
|
import { LogEngine, LogMode } from "@wgtechlabs/log-engine";
|
|
@@ -1141,19 +1222,22 @@ function extractJson(raw) {
|
|
|
1141
1222
|
}
|
|
1142
1223
|
return text2;
|
|
1143
1224
|
}
|
|
1144
|
-
async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit") {
|
|
1225
|
+
async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit", context) {
|
|
1145
1226
|
try {
|
|
1146
1227
|
const isLarge = stagedFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
|
|
1147
1228
|
const multiFileHint = stagedFiles.length > 1 ? `
|
|
1148
1229
|
|
|
1149
1230
|
IMPORTANT: Multiple files are staged. Generate ONE commit message that captures the high-level purpose of ALL changes together. Focus on the overall intent, not individual file changes. Be specific but concise — do not list every file.` : "";
|
|
1231
|
+
const squashHint = context === "squash-merge" ? `
|
|
1232
|
+
|
|
1233
|
+
CONTEXT: This is a squash merge of an entire feature branch into the base branch. All commits are being combined into ONE single commit. Generate a single high-level summary that describes the overall feature or change — NOT a list of individual commits. Think: what capability was added or what problem was solved? Be specific but concise.` : "";
|
|
1150
1234
|
const diffContent = isLarge ? createCompactDiff(stagedFiles, diff) : diff.slice(0, 4000);
|
|
1151
1235
|
const userMessage = `Generate a commit message for these staged changes:
|
|
1152
1236
|
|
|
1153
1237
|
Files (${stagedFiles.length}): ${stagedFiles.join(", ")}
|
|
1154
1238
|
|
|
1155
1239
|
Diff:
|
|
1156
|
-
${diffContent}${multiFileHint}`;
|
|
1240
|
+
${diffContent}${multiFileHint}${squashHint}`;
|
|
1157
1241
|
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model, isLarge ? COPILOT_LONG_TIMEOUT_MS : COPILOT_TIMEOUT_MS);
|
|
1158
1242
|
return result?.trim() ?? null;
|
|
1159
1243
|
} catch {
|
|
@@ -1885,6 +1969,27 @@ ${pc6.bold("Changed files:")}`);
|
|
|
1885
1969
|
}
|
|
1886
1970
|
}
|
|
1887
1971
|
info(`Staged files: ${stagedFiles.join(", ")}`);
|
|
1972
|
+
const LARGE_COMMIT_THRESHOLD = 10;
|
|
1973
|
+
if (stagedFiles.length >= LARGE_COMMIT_THRESHOLD && !args.group) {
|
|
1974
|
+
const dirs = new Set(stagedFiles.map((f) => f.split("/")[0]));
|
|
1975
|
+
if (dirs.size > 1) {
|
|
1976
|
+
console.log();
|
|
1977
|
+
warn(`You're staging ${pc6.bold(String(stagedFiles.length))} files across ${pc6.bold(String(dirs.size))} directories in a single commit.`);
|
|
1978
|
+
info(pc6.dim("Large commits mixing different topics make history harder to read and bisect. " + "For cleaner history, consider splitting into atomic commits."));
|
|
1979
|
+
const choice = await selectPrompt("How would you like to proceed?", [
|
|
1980
|
+
"Continue as single commit",
|
|
1981
|
+
"Switch to group mode (AI splits into atomic commits)",
|
|
1982
|
+
"Cancel"
|
|
1983
|
+
]);
|
|
1984
|
+
if (choice === "Cancel") {
|
|
1985
|
+
process.exit(0);
|
|
1986
|
+
}
|
|
1987
|
+
if (choice === "Switch to group mode (AI splits into atomic commits)") {
|
|
1988
|
+
await runGroupCommit(args.model, config);
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1888
1993
|
let commitMessage = null;
|
|
1889
1994
|
const useAI = !args["no-ai"];
|
|
1890
1995
|
if (useAI) {
|
|
@@ -2163,7 +2268,7 @@ import pc7 from "picocolors";
|
|
|
2163
2268
|
// package.json
|
|
2164
2269
|
var package_default = {
|
|
2165
2270
|
name: "contribute-now",
|
|
2166
|
-
version: "0.
|
|
2271
|
+
version: "0.5.0-dev.9ad3d01",
|
|
2167
2272
|
description: "Developer CLI that automates git workflows — branching, syncing, committing, and PRs — with multi-workflow and commit convention support.",
|
|
2168
2273
|
type: "module",
|
|
2169
2274
|
bin: {
|
|
@@ -2181,9 +2286,10 @@ var package_default = {
|
|
|
2181
2286
|
lint: "biome check .",
|
|
2182
2287
|
"lint:fix": "biome check --write .",
|
|
2183
2288
|
format: "biome format --write .",
|
|
2184
|
-
"
|
|
2185
|
-
"
|
|
2186
|
-
"
|
|
2289
|
+
"landing:install": "bun install --cwd landing",
|
|
2290
|
+
"landing:dev": "bun run --cwd landing dev",
|
|
2291
|
+
"landing:build": "bun run --cwd landing build",
|
|
2292
|
+
"landing:preview": "bun run --cwd landing preview"
|
|
2187
2293
|
},
|
|
2188
2294
|
engines: {
|
|
2189
2295
|
node: ">=18",
|
|
@@ -2681,7 +2787,19 @@ var log_default = defineCommand6({
|
|
|
2681
2787
|
all: {
|
|
2682
2788
|
type: "boolean",
|
|
2683
2789
|
alias: "a",
|
|
2684
|
-
description: "Show all branches
|
|
2790
|
+
description: "Show commits from all branches",
|
|
2791
|
+
default: false
|
|
2792
|
+
},
|
|
2793
|
+
remote: {
|
|
2794
|
+
type: "boolean",
|
|
2795
|
+
alias: "r",
|
|
2796
|
+
description: "Show only remote commits not yet pulled locally",
|
|
2797
|
+
default: false
|
|
2798
|
+
},
|
|
2799
|
+
full: {
|
|
2800
|
+
type: "boolean",
|
|
2801
|
+
alias: "f",
|
|
2802
|
+
description: "Show full commit history for the current branch",
|
|
2685
2803
|
default: false
|
|
2686
2804
|
},
|
|
2687
2805
|
graph: {
|
|
@@ -2703,44 +2821,197 @@ var log_default = defineCommand6({
|
|
|
2703
2821
|
}
|
|
2704
2822
|
const config = readConfig();
|
|
2705
2823
|
const count = args.count ? Number.parseInt(args.count, 10) : 20;
|
|
2706
|
-
const showAll = args.all;
|
|
2707
2824
|
const showGraph = args.graph;
|
|
2708
2825
|
const targetBranch = args.branch;
|
|
2826
|
+
let mode = "local";
|
|
2827
|
+
if (args.all)
|
|
2828
|
+
mode = "all";
|
|
2829
|
+
else if (args.remote)
|
|
2830
|
+
mode = "remote";
|
|
2831
|
+
else if (args.full || targetBranch)
|
|
2832
|
+
mode = "full";
|
|
2709
2833
|
const protectedBranches = config ? getProtectedBranches(config) : ["main", "master"];
|
|
2710
2834
|
const currentBranch = await getCurrentBranch();
|
|
2835
|
+
const upstream = await getUpstreamRef();
|
|
2836
|
+
let compareRef = upstream;
|
|
2837
|
+
let usingFallback = false;
|
|
2838
|
+
if (!compareRef) {
|
|
2839
|
+
const fallback = await resolveBaseBranchRef(config);
|
|
2840
|
+
if (fallback) {
|
|
2841
|
+
compareRef = fallback;
|
|
2842
|
+
usingFallback = true;
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2711
2845
|
heading("\uD83D\uDCDC commit log");
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
if (
|
|
2715
|
-
console.log(pc9.dim(" No commits found."));
|
|
2846
|
+
printModeHeader(mode, currentBranch, compareRef, usingFallback);
|
|
2847
|
+
if (mode === "local" || mode === "remote") {
|
|
2848
|
+
if (!compareRef) {
|
|
2716
2849
|
console.log();
|
|
2850
|
+
console.log(pc9.yellow(" ⚠ Could not determine a comparison branch."));
|
|
2851
|
+
console.log(pc9.dim(" No upstream tracking set and no remote base branch found."));
|
|
2852
|
+
console.log(pc9.dim(` Use ${pc9.bold("contrib log --full")} to see the full commit history instead.`));
|
|
2853
|
+
console.log();
|
|
2854
|
+
printGuidance();
|
|
2717
2855
|
return;
|
|
2718
2856
|
}
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2857
|
+
const hasCommits = await renderScopedLog({ mode, count, upstream: compareRef, showGraph, protectedBranches, currentBranch });
|
|
2858
|
+
if (!hasCommits) {
|
|
2859
|
+
printGuidance();
|
|
2860
|
+
return;
|
|
2722
2861
|
}
|
|
2723
2862
|
} else {
|
|
2724
|
-
const
|
|
2725
|
-
if (
|
|
2726
|
-
|
|
2727
|
-
console.log();
|
|
2863
|
+
const hasCommits = await renderFullLog({ count, all: mode === "all", showGraph, targetBranch, protectedBranches, currentBranch });
|
|
2864
|
+
if (!hasCommits) {
|
|
2865
|
+
printGuidance();
|
|
2728
2866
|
return;
|
|
2729
2867
|
}
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2868
|
+
}
|
|
2869
|
+
printFooter(mode, count, targetBranch);
|
|
2870
|
+
printGuidance();
|
|
2871
|
+
}
|
|
2872
|
+
});
|
|
2873
|
+
async function resolveBaseBranchRef(config) {
|
|
2874
|
+
if (!config) {
|
|
2875
|
+
for (const candidate2 of ["origin/main", "origin/master"]) {
|
|
2876
|
+
if (await branchExists(candidate2))
|
|
2877
|
+
return candidate2;
|
|
2878
|
+
}
|
|
2879
|
+
return null;
|
|
2880
|
+
}
|
|
2881
|
+
const baseBranch = getBaseBranch(config);
|
|
2882
|
+
const remote = config.origin ?? "origin";
|
|
2883
|
+
const candidate = `${remote}/${baseBranch}`;
|
|
2884
|
+
if (await branchExists(candidate))
|
|
2885
|
+
return candidate;
|
|
2886
|
+
for (const fallback of ["origin/main", "origin/master"]) {
|
|
2887
|
+
if (fallback !== candidate && await branchExists(fallback))
|
|
2888
|
+
return fallback;
|
|
2889
|
+
}
|
|
2890
|
+
return null;
|
|
2891
|
+
}
|
|
2892
|
+
function printModeHeader(mode, currentBranch, compareRef, usingFallback = false) {
|
|
2893
|
+
const branch = currentBranch ?? "HEAD";
|
|
2894
|
+
const fallbackNote = usingFallback ? pc9.yellow(" (no upstream — comparing against base branch)") : "";
|
|
2895
|
+
console.log();
|
|
2896
|
+
switch (mode) {
|
|
2897
|
+
case "local":
|
|
2898
|
+
console.log(pc9.dim(` mode: ${pc9.bold("local")} — unpushed commits on ${pc9.bold(branch)}`) + fallbackNote);
|
|
2899
|
+
if (compareRef) {
|
|
2900
|
+
console.log(pc9.dim(` comparing: ${pc9.bold(compareRef)} ➜ ${pc9.bold("HEAD")}`));
|
|
2901
|
+
}
|
|
2902
|
+
break;
|
|
2903
|
+
case "remote":
|
|
2904
|
+
console.log(pc9.dim(` mode: ${pc9.bold("remote")} — commits on remote not yet pulled into ${pc9.bold(branch)}`) + fallbackNote);
|
|
2905
|
+
if (compareRef) {
|
|
2906
|
+
console.log(pc9.dim(` comparing: ${pc9.bold("HEAD")} ➜ ${pc9.bold(compareRef)}`));
|
|
2736
2907
|
}
|
|
2908
|
+
break;
|
|
2909
|
+
case "full":
|
|
2910
|
+
console.log(pc9.dim(` mode: ${pc9.bold("full")} — complete commit history for ${pc9.bold(branch)}`));
|
|
2911
|
+
break;
|
|
2912
|
+
case "all":
|
|
2913
|
+
console.log(pc9.dim(` mode: ${pc9.bold("all")} — commits across all branches`));
|
|
2914
|
+
break;
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
async function renderScopedLog(options) {
|
|
2918
|
+
const { mode, count, upstream, showGraph, protectedBranches, currentBranch } = options;
|
|
2919
|
+
if (showGraph) {
|
|
2920
|
+
const graphFn = mode === "local" ? getLocalCommitsGraph : getRemoteOnlyCommitsGraph;
|
|
2921
|
+
const lines = await graphFn({ count, upstream });
|
|
2922
|
+
if (lines.length === 0) {
|
|
2923
|
+
printEmptyState(mode);
|
|
2924
|
+
return false;
|
|
2737
2925
|
}
|
|
2738
2926
|
console.log();
|
|
2739
|
-
|
|
2740
|
-
|
|
2927
|
+
for (const line of lines) {
|
|
2928
|
+
console.log(` ${colorizeGraphLine(line, protectedBranches, currentBranch)}`);
|
|
2929
|
+
}
|
|
2930
|
+
} else {
|
|
2931
|
+
const entryFn = mode === "local" ? getLocalCommitsEntries : getRemoteOnlyCommitsEntries;
|
|
2932
|
+
const entries = await entryFn({ count, upstream });
|
|
2933
|
+
if (entries.length === 0) {
|
|
2934
|
+
printEmptyState(mode);
|
|
2935
|
+
return false;
|
|
2936
|
+
}
|
|
2741
2937
|
console.log();
|
|
2938
|
+
for (const entry of entries) {
|
|
2939
|
+
const hashStr = pc9.yellow(entry.hash);
|
|
2940
|
+
const refsStr = entry.refs ? ` ${colorizeRefs(entry.refs, protectedBranches, currentBranch)}` : "";
|
|
2941
|
+
const subjectStr = colorizeSubject(entry.subject);
|
|
2942
|
+
console.log(` ${hashStr}${refsStr} ${subjectStr}`);
|
|
2943
|
+
}
|
|
2742
2944
|
}
|
|
2743
|
-
|
|
2945
|
+
return true;
|
|
2946
|
+
}
|
|
2947
|
+
function printEmptyState(mode) {
|
|
2948
|
+
console.log();
|
|
2949
|
+
if (mode === "local") {
|
|
2950
|
+
console.log(pc9.dim(" No local unpushed commits — you're up to date with remote!"));
|
|
2951
|
+
} else {
|
|
2952
|
+
console.log(pc9.dim(" No remote-only commits — your local branch is up to date!"));
|
|
2953
|
+
}
|
|
2954
|
+
console.log();
|
|
2955
|
+
}
|
|
2956
|
+
async function renderFullLog(options) {
|
|
2957
|
+
const { count, all, showGraph, targetBranch, protectedBranches, currentBranch } = options;
|
|
2958
|
+
if (showGraph) {
|
|
2959
|
+
const lines = await getLogGraph({ count, all, branch: targetBranch });
|
|
2960
|
+
if (lines.length === 0) {
|
|
2961
|
+
console.log(pc9.dim(" No commits found."));
|
|
2962
|
+
console.log();
|
|
2963
|
+
return false;
|
|
2964
|
+
}
|
|
2965
|
+
console.log();
|
|
2966
|
+
for (const line of lines) {
|
|
2967
|
+
console.log(` ${colorizeGraphLine(line, protectedBranches, currentBranch)}`);
|
|
2968
|
+
}
|
|
2969
|
+
} else {
|
|
2970
|
+
const entries = await getLogEntries({ count, all, branch: targetBranch });
|
|
2971
|
+
if (entries.length === 0) {
|
|
2972
|
+
console.log(pc9.dim(" No commits found."));
|
|
2973
|
+
console.log();
|
|
2974
|
+
return false;
|
|
2975
|
+
}
|
|
2976
|
+
console.log();
|
|
2977
|
+
for (const entry of entries) {
|
|
2978
|
+
const hashStr = pc9.yellow(entry.hash);
|
|
2979
|
+
const refsStr = entry.refs ? ` ${colorizeRefs(entry.refs, protectedBranches, currentBranch)}` : "";
|
|
2980
|
+
const subjectStr = colorizeSubject(entry.subject);
|
|
2981
|
+
console.log(` ${hashStr}${refsStr} ${subjectStr}`);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
return true;
|
|
2985
|
+
}
|
|
2986
|
+
function printFooter(mode, count, targetBranch) {
|
|
2987
|
+
console.log();
|
|
2988
|
+
switch (mode) {
|
|
2989
|
+
case "local":
|
|
2990
|
+
console.log(pc9.dim(` Showing up to ${count} unpushed commits`));
|
|
2991
|
+
break;
|
|
2992
|
+
case "remote":
|
|
2993
|
+
console.log(pc9.dim(` Showing up to ${count} remote-only commits`));
|
|
2994
|
+
break;
|
|
2995
|
+
case "full":
|
|
2996
|
+
console.log(pc9.dim(` Showing ${count} most recent commits${targetBranch ? ` (${targetBranch})` : ""}`));
|
|
2997
|
+
break;
|
|
2998
|
+
case "all":
|
|
2999
|
+
console.log(pc9.dim(` Showing ${count} most recent commits (all branches)`));
|
|
3000
|
+
break;
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
function printGuidance() {
|
|
3004
|
+
console.log();
|
|
3005
|
+
console.log(pc9.dim(" ─── quick guide ───"));
|
|
3006
|
+
console.log(pc9.dim(` ${pc9.bold("contrib log")} local unpushed commits (default)`));
|
|
3007
|
+
console.log(pc9.dim(` ${pc9.bold("contrib log --remote")} commits on remote not yet pulled`));
|
|
3008
|
+
console.log(pc9.dim(` ${pc9.bold("contrib log --full")} full history for the current branch`));
|
|
3009
|
+
console.log(pc9.dim(` ${pc9.bold("contrib log --all")} commits across all branches`));
|
|
3010
|
+
console.log(pc9.dim(` ${pc9.bold("contrib log -n 50")} change the commit limit (default: 20)`));
|
|
3011
|
+
console.log(pc9.dim(` ${pc9.bold("contrib log -b dev")} view log for a specific branch`));
|
|
3012
|
+
console.log(pc9.dim(` ${pc9.bold("contrib log --no-graph")} flat list without graph lines`));
|
|
3013
|
+
console.log();
|
|
3014
|
+
}
|
|
2744
3015
|
function colorizeGraphLine(line, protectedBranches, currentBranch) {
|
|
2745
3016
|
const match = line.match(/^([|/\\*\s_.-]*)([a-f0-9]{7,12})(\s+\(([^)]+)\))?\s*(.*)/);
|
|
2746
3017
|
if (!match) {
|
|
@@ -3221,10 +3492,20 @@ var status_default = defineCommand9({
|
|
|
3221
3492
|
const devLine = formatStatus(config.devBranch, devRemoteRef, devDiv.ahead, devDiv.behind);
|
|
3222
3493
|
console.log(devLine);
|
|
3223
3494
|
}
|
|
3224
|
-
|
|
3495
|
+
const protectedBranches = getProtectedBranches(config);
|
|
3496
|
+
const isFeatureBranch = currentBranch && !protectedBranches.includes(currentBranch);
|
|
3497
|
+
let branchStatus = null;
|
|
3498
|
+
if (isFeatureBranch) {
|
|
3225
3499
|
const branchDiv = await getDivergence(currentBranch, baseBranch);
|
|
3226
3500
|
const branchLine = formatStatus(currentBranch, baseBranch, branchDiv.ahead, branchDiv.behind);
|
|
3227
3501
|
console.log(branchLine + pc12.dim(` (current ${pc12.green("*")})`));
|
|
3502
|
+
branchStatus = await detectBranchStatus(currentBranch, baseBranch);
|
|
3503
|
+
if (branchStatus.merged) {
|
|
3504
|
+
console.log(` ${pc12.green("✓")} ${pc12.green("Branch merged")} — ${pc12.dim(branchStatus.mergedReason ?? "all commits reachable from base")}`);
|
|
3505
|
+
}
|
|
3506
|
+
if (branchStatus.stale) {
|
|
3507
|
+
console.log(` ${pc12.yellow("⏳")} ${pc12.yellow("Branch is stale")} — ${pc12.dim(`last commit ${branchStatus.staleDaysAgo} days ago`)}`);
|
|
3508
|
+
}
|
|
3228
3509
|
} else if (currentBranch) {
|
|
3229
3510
|
console.log(pc12.dim(` (on ${pc12.bold(currentBranch)} branch)`));
|
|
3230
3511
|
}
|
|
@@ -3259,10 +3540,16 @@ var status_default = defineCommand9({
|
|
|
3259
3540
|
if (fileStatus.modified.length > 0 || fileStatus.untracked.length > 0) {
|
|
3260
3541
|
tips.push(`Run ${pc12.bold("contrib commit")} to stage and commit changes`);
|
|
3261
3542
|
}
|
|
3262
|
-
if (
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3543
|
+
if (isFeatureBranch && branchStatus) {
|
|
3544
|
+
if (branchStatus.merged) {
|
|
3545
|
+
tips.push(`Run ${pc12.bold("contrib clean")} to delete this merged branch`);
|
|
3546
|
+
} else if (branchStatus.stale) {
|
|
3547
|
+
tips.push(`Run ${pc12.bold("contrib sync")} to rebase on latest changes, or ${pc12.bold("contrib clean")} if no longer needed`);
|
|
3548
|
+
} else if (fileStatus.staged.length === 0 && fileStatus.modified.length === 0 && fileStatus.untracked.length === 0) {
|
|
3549
|
+
const branchDiv = await getDivergence(currentBranch, `${origin}/${currentBranch}`);
|
|
3550
|
+
if (branchDiv.ahead > 0) {
|
|
3551
|
+
tips.push(`Run ${pc12.bold("contrib submit")} to push and create/update your PR`);
|
|
3552
|
+
}
|
|
3266
3553
|
}
|
|
3267
3554
|
}
|
|
3268
3555
|
if (tips.length > 0) {
|
|
@@ -3288,6 +3575,48 @@ function formatStatus(branch, base, ahead, behind) {
|
|
|
3288
3575
|
}
|
|
3289
3576
|
return ` ${pc12.red("⚡")} ${label} ${pc12.yellow(`${ahead} ahead`)}${pc12.dim(", ")}${pc12.red(`${behind} behind`)} ${pc12.dim(base)}`;
|
|
3290
3577
|
}
|
|
3578
|
+
var STALE_THRESHOLD_DAYS = 14;
|
|
3579
|
+
async function detectBranchStatus(branch, baseBranch) {
|
|
3580
|
+
const result = { merged: false, mergedReason: null, stale: false, staleDaysAgo: null };
|
|
3581
|
+
const div = await getDivergence(branch, baseBranch);
|
|
3582
|
+
const hasWork = div.ahead > 0;
|
|
3583
|
+
if (hasWork) {
|
|
3584
|
+
if (await isBranchMergedInto(branch, baseBranch)) {
|
|
3585
|
+
result.merged = true;
|
|
3586
|
+
result.mergedReason = `all commits reachable from ${baseBranch}`;
|
|
3587
|
+
return result;
|
|
3588
|
+
}
|
|
3589
|
+
const mergedBranches = await getMergedBranches(baseBranch);
|
|
3590
|
+
if (mergedBranches.includes(branch)) {
|
|
3591
|
+
result.merged = true;
|
|
3592
|
+
result.mergedReason = `listed in merged branches of ${baseBranch}`;
|
|
3593
|
+
return result;
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
const goneBranches = await getGoneBranches();
|
|
3597
|
+
if (goneBranches.includes(branch)) {
|
|
3598
|
+
result.merged = true;
|
|
3599
|
+
result.mergedReason = "remote branch deleted (likely squash-merged)";
|
|
3600
|
+
return result;
|
|
3601
|
+
}
|
|
3602
|
+
if (await checkGhInstalled()) {
|
|
3603
|
+
const mergedPR = await getMergedPRForBranch(branch);
|
|
3604
|
+
if (mergedPR) {
|
|
3605
|
+
result.merged = true;
|
|
3606
|
+
result.mergedReason = `PR #${mergedPR.number} was merged`;
|
|
3607
|
+
return result;
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
const lastDate = await getLastCommitDate(branch);
|
|
3611
|
+
if (lastDate) {
|
|
3612
|
+
const daysAgo = Math.floor((Date.now() - new Date(lastDate).getTime()) / (1000 * 60 * 60 * 24));
|
|
3613
|
+
if (daysAgo >= STALE_THRESHOLD_DAYS) {
|
|
3614
|
+
result.stale = true;
|
|
3615
|
+
result.staleDaysAgo = daysAgo;
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
return result;
|
|
3619
|
+
}
|
|
3291
3620
|
|
|
3292
3621
|
// src/commands/submit.ts
|
|
3293
3622
|
import { defineCommand as defineCommand10 } from "citty";
|
|
@@ -3311,10 +3640,12 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
|
|
|
3311
3640
|
if (!copilotError) {
|
|
3312
3641
|
const spinner = createSpinner("Generating AI commit message for squash merge...");
|
|
3313
3642
|
const [stagedDiff, stagedFiles] = await Promise.all([getStagedDiff(), getStagedFiles()]);
|
|
3314
|
-
const aiMsg = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit");
|
|
3643
|
+
const aiMsg = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit", "squash-merge");
|
|
3315
3644
|
if (aiMsg) {
|
|
3316
3645
|
message = aiMsg;
|
|
3317
3646
|
spinner.success("AI commit message generated.");
|
|
3647
|
+
console.log(`
|
|
3648
|
+
${pc13.dim("AI suggestion:")} ${pc13.bold(pc13.cyan(message))}`);
|
|
3318
3649
|
} else {
|
|
3319
3650
|
spinner.fail("AI did not return a commit message.");
|
|
3320
3651
|
}
|
|
@@ -3322,13 +3653,38 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
|
|
|
3322
3653
|
warn(`AI unavailable: ${copilotError}`);
|
|
3323
3654
|
}
|
|
3324
3655
|
}
|
|
3325
|
-
|
|
3326
|
-
let finalMsg;
|
|
3656
|
+
let finalMsg = null;
|
|
3327
3657
|
if (message) {
|
|
3328
|
-
|
|
3329
|
-
|
|
3658
|
+
while (!finalMsg) {
|
|
3659
|
+
const action = await selectPrompt("What would you like to do?", [
|
|
3660
|
+
"Accept this message",
|
|
3661
|
+
"Edit this message",
|
|
3662
|
+
"Regenerate",
|
|
3663
|
+
"Write manually"
|
|
3664
|
+
]);
|
|
3665
|
+
if (action === "Accept this message") {
|
|
3666
|
+
finalMsg = message;
|
|
3667
|
+
} else if (action === "Edit this message") {
|
|
3668
|
+
finalMsg = await inputPrompt("Edit commit message", message);
|
|
3669
|
+
} else if (action === "Regenerate") {
|
|
3670
|
+
const spinner = createSpinner("Regenerating commit message...");
|
|
3671
|
+
const [stagedDiff, stagedFiles] = await Promise.all([getStagedDiff(), getStagedFiles()]);
|
|
3672
|
+
const regen = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit", "squash-merge");
|
|
3673
|
+
if (regen) {
|
|
3674
|
+
message = regen;
|
|
3675
|
+
spinner.success("Commit message regenerated.");
|
|
3676
|
+
console.log(`
|
|
3677
|
+
${pc13.dim("AI suggestion:")} ${pc13.bold(pc13.cyan(regen))}`);
|
|
3678
|
+
} else {
|
|
3679
|
+
spinner.fail("Regeneration failed.");
|
|
3680
|
+
finalMsg = await inputPrompt("Enter commit message");
|
|
3681
|
+
}
|
|
3682
|
+
} else {
|
|
3683
|
+
finalMsg = await inputPrompt("Enter commit message");
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3330
3686
|
} else {
|
|
3331
|
-
finalMsg = await inputPrompt("Commit message",
|
|
3687
|
+
finalMsg = await inputPrompt("Commit message", `squash merge ${featureBranch}`);
|
|
3332
3688
|
}
|
|
3333
3689
|
const commitResult = await commitWithMessage(finalMsg);
|
|
3334
3690
|
if (commitResult.exitCode !== 0) {
|
|
@@ -3581,6 +3937,25 @@ var submit_default = defineCommand10({
|
|
|
3581
3937
|
return;
|
|
3582
3938
|
}
|
|
3583
3939
|
}
|
|
3940
|
+
if (ghInstalled && ghAuthed) {
|
|
3941
|
+
const existingPR = await getPRForBranch(currentBranch);
|
|
3942
|
+
if (existingPR) {
|
|
3943
|
+
info(`Pushing ${pc13.bold(currentBranch)} to ${origin}...`);
|
|
3944
|
+
const pushResult2 = await pushSetUpstream(origin, currentBranch);
|
|
3945
|
+
if (pushResult2.exitCode !== 0) {
|
|
3946
|
+
error(`Failed to push: ${pushResult2.stderr}`);
|
|
3947
|
+
if (pushResult2.stderr.includes("rejected") || pushResult2.stderr.includes("non-fast-forward")) {
|
|
3948
|
+
warn("The remote branch has diverged. Try:");
|
|
3949
|
+
info(` git pull --rebase ${origin} ${currentBranch}`);
|
|
3950
|
+
info(" Then run `contrib submit` again.");
|
|
3951
|
+
}
|
|
3952
|
+
process.exit(1);
|
|
3953
|
+
}
|
|
3954
|
+
success(`Pushed changes to existing PR #${existingPR.number}: ${pc13.bold(existingPR.title)}`);
|
|
3955
|
+
console.log(` ${pc13.cyan(existingPR.url)}`);
|
|
3956
|
+
return;
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3584
3959
|
let prTitle = null;
|
|
3585
3960
|
let prBody = null;
|
|
3586
3961
|
async function tryGenerateAI() {
|
|
@@ -3683,7 +4058,6 @@ ${pc13.dim("AI body preview:")}`);
|
|
|
3683
4058
|
}
|
|
3684
4059
|
if (submitAction === "squash") {
|
|
3685
4060
|
await performSquashMerge(origin, baseBranch, currentBranch, {
|
|
3686
|
-
defaultMsg: prTitle ?? undefined,
|
|
3687
4061
|
model: args.model,
|
|
3688
4062
|
convention: config.commitConvention
|
|
3689
4063
|
});
|
|
@@ -3714,12 +4088,6 @@ ${pc13.dim("AI body preview:")}`);
|
|
|
3714
4088
|
}
|
|
3715
4089
|
return;
|
|
3716
4090
|
}
|
|
3717
|
-
const existingPR = await getPRForBranch(currentBranch);
|
|
3718
|
-
if (existingPR) {
|
|
3719
|
-
success(`Pushed changes to existing PR #${existingPR.number}: ${pc13.bold(existingPR.title)}`);
|
|
3720
|
-
console.log(` ${pc13.cyan(existingPR.url)}`);
|
|
3721
|
-
return;
|
|
3722
|
-
}
|
|
3723
4091
|
if (submitAction === "fill") {
|
|
3724
4092
|
const fillResult = await createPRFill(baseBranch, args.draft);
|
|
3725
4093
|
if (fillResult.exitCode !== 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "contribute-now",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-dev.9ad3d01",
|
|
4
4
|
"description": "Developer CLI that automates git workflows — branching, syncing, committing, and PRs — with multi-workflow and commit convention support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
"lint": "biome check .",
|
|
19
19
|
"lint:fix": "biome check --write .",
|
|
20
20
|
"format": "biome format --write .",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
21
|
+
"landing:install": "bun install --cwd landing",
|
|
22
|
+
"landing:dev": "bun run --cwd landing dev",
|
|
23
|
+
"landing:build": "bun run --cwd landing build",
|
|
24
|
+
"landing:preview": "bun run --cwd landing preview"
|
|
24
25
|
},
|
|
25
26
|
"engines": {
|
|
26
27
|
"node": ">=18",
|