git-coco 0.1.1 → 0.2.1
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 +10 -2
- package/dist/index.esm.mjs +139 -251
- package/dist/index.esm.mjs.map +1 -1
- package/dist/index.js +138 -252
- package/dist/lib/parsers/noResult.d.ts +4 -6
- package/dist/lib/simple-git/createCommit.d.ts +2 -0
- package/dist/lib/simple-git/getChanges.d.ts +12 -0
- package/dist/lib/simple-git/getDiff.d.ts +7 -0
- package/dist/lib/simple-git/getStatus.d.ts +3 -0
- package/dist/lib/simple-git/getSummaryText.d.ts +3 -0
- package/dist/lib/types.d.ts +3 -3
- package/dist/stats.html +1 -1
- package/package.json +10 -11
- package/dist/lib/parsers/default/utils/parseFileDiff.d.ts +0 -4
- package/dist/lib/utils/git/constants.d.ts +0 -1
- package/dist/lib/utils/git/createCommit.d.ts +0 -2
- package/dist/lib/utils/git/getChanges.d.ts +0 -43
- package/dist/lib/utils/git/getStatus.d.ts +0 -3
- package/dist/lib/utils/git/getSummaryText.d.ts +0 -2
- package/dist/lib/utils/git/parsePatches.d.ts +0 -18
- /package/dist/lib/parsers/default/{fileChangeParser.d.ts → index.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
# `coco`
|
|
1
|
+
# `coco` 🤖 🦍
|
|
2
|
+
|
|
3
|
+
[](https://github.com/gfargo/coco/issues)
|
|
4
|
+
[](https://github.com/gfargo/coco/pulls)
|
|
5
|
+
[](https://github.com/gfargo/coco/tree/main)
|
|
6
|
+
[](https://www.npmjs.com/package/git-coco)
|
|
7
|
+
[](https://www.npmjs.com/package/git-coco)
|
|
8
|
+
|
|
9
|
+
Commit Copilot, or `coco`, is your personal scribe for git commit messages. Leveraging the power of [LangChain🦜🔗](https://js.langchain.com/) and LLMs to encapsulate your staged changes into meaningful commit messages!
|
|
10
|
+
|
|
2
11
|
|
|
3
|
-
Commit Copilot, or `coco`, is your personal scribe for git commit messages. Using [LangChain🦜🔗](https://js.langchain.com/) to automate the task of creating meaningful commit messages based on your staged changes!
|
|
4
12
|
|
|
5
13
|
## Installation
|
|
6
14
|
|
package/dist/index.esm.mjs
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import * as nodegit from 'nodegit';
|
|
3
|
-
import { Diff, Blob, Tree, Repository } from 'nodegit';
|
|
4
2
|
import { select, editor } from '@inquirer/prompts';
|
|
5
3
|
import * as fs from 'fs';
|
|
6
4
|
import * as os from 'os';
|
|
7
5
|
import * as path from 'path';
|
|
6
|
+
import path__default from 'path';
|
|
8
7
|
import * as ini from 'ini';
|
|
9
8
|
import yargs from 'yargs';
|
|
10
9
|
import { hideBin } from 'yargs/helpers';
|
|
@@ -15,13 +14,13 @@ import ora from 'ora';
|
|
|
15
14
|
import now from 'performance-now';
|
|
16
15
|
import prettyMilliseconds from 'pretty-ms';
|
|
17
16
|
import { Document } from 'langchain/document';
|
|
18
|
-
import { createTwoFilesPatch } from 'diff';
|
|
19
|
-
import * as util from 'util';
|
|
20
17
|
import { loadSummarizationChain, LLMChain } from 'langchain/chains';
|
|
21
18
|
import { OpenAI } from 'langchain/llms/openai';
|
|
22
19
|
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
|
|
20
|
+
import { createTwoFilesPatch } from 'diff';
|
|
23
21
|
import GPT3NodeTokenizer from 'gpt3-tokenizer';
|
|
24
22
|
import { minimatch } from 'minimatch';
|
|
23
|
+
import { simpleGit } from 'simple-git';
|
|
25
24
|
|
|
26
25
|
/**
|
|
27
26
|
* Returns a new object with all undefined keys removed
|
|
@@ -419,7 +418,7 @@ const defaultOutputCallback = (group) => {
|
|
|
419
418
|
let output = `
|
|
420
419
|
-------\n* changes in "/${group.path}"\n\n`;
|
|
421
420
|
if (group.summary) {
|
|
422
|
-
output += `${group.diffs.map((diff) => ` • ${diff.summary}`).join('\n')}\n\nSummary
|
|
421
|
+
output += `${group.diffs.map((diff) => ` • ${diff.summary}`).join('\n')}\n\nSummary:\n\n${group.summary}\n\n`;
|
|
423
422
|
}
|
|
424
423
|
else {
|
|
425
424
|
output += `${group.diffs.map((diff) => ` • ${diff.summary}\n\n${diff.diff}`).join('\n\n')}\n\n`;
|
|
@@ -543,61 +542,6 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger = new Logger(co
|
|
|
543
542
|
};
|
|
544
543
|
}
|
|
545
544
|
|
|
546
|
-
const readFile = util.promisify(fs.readFile);
|
|
547
|
-
|
|
548
|
-
const parseDefaultFileDiff = async (nodeFile, repo, headTree, index) => {
|
|
549
|
-
let result = '';
|
|
550
|
-
const diff = await Diff.treeToIndex(repo, headTree, index, {
|
|
551
|
-
flags: 33554432 /* Diff.OPTION.SHOW_UNTRACKED_CONTENT */ | 16 /* Diff.OPTION.RECURSE_UNTRACKED_DIRS */,
|
|
552
|
-
pathspec: nodeFile.filepath,
|
|
553
|
-
});
|
|
554
|
-
const patches = await diff.patches();
|
|
555
|
-
for (const patch of patches) {
|
|
556
|
-
const hunks = await patch.hunks();
|
|
557
|
-
for (const hunk of hunks) {
|
|
558
|
-
const lines = await hunk.lines();
|
|
559
|
-
result += lines.map((line) => String.fromCharCode(line.origin()) + line.content()).join('');
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
return result;
|
|
563
|
-
};
|
|
564
|
-
const parseRenamedFileDiff = async (nodeFile, repo, headTree, index, logger) => {
|
|
565
|
-
let result = '';
|
|
566
|
-
const oldFilepath = nodeFile?.oldFilepath || nodeFile.filepath;
|
|
567
|
-
try {
|
|
568
|
-
const headEntry = await headTree.entryByPath(oldFilepath); // use old name to look up in latest commit
|
|
569
|
-
const indexEntry = index.getByPath(nodeFile.filepath); // use new name to look up in index
|
|
570
|
-
// Compare the file contents in the latest commit and index
|
|
571
|
-
const headBlob = await Blob.lookup(repo, headEntry.sha());
|
|
572
|
-
const indexBlobContent = await readFile(indexEntry.path); // read file from filesystem
|
|
573
|
-
const headContent = headBlob.content().toString();
|
|
574
|
-
const indexContent = indexBlobContent.toString();
|
|
575
|
-
if (headContent !== indexContent) {
|
|
576
|
-
result = createTwoFilesPatch(oldFilepath, nodeFile.filepath, headContent, indexContent, '', '', { context: 3 });
|
|
577
|
-
// remove the first 4 lines of the patch (they contain the old and new file names)
|
|
578
|
-
result = result.split('\n').slice(4).join('\n');
|
|
579
|
-
}
|
|
580
|
-
else {
|
|
581
|
-
result = 'File contents are unchanged.';
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
catch (err) {
|
|
585
|
-
logger.verbose(`Error comparing file contents for ${nodeFile.filepath}`, { color: 'red' });
|
|
586
|
-
result = 'Error comparing file contents.';
|
|
587
|
-
}
|
|
588
|
-
return result;
|
|
589
|
-
};
|
|
590
|
-
const parseFileDiff = async (nodeFile, repo, headTree, index, logger) => {
|
|
591
|
-
if (nodeFile.status === 'deleted') {
|
|
592
|
-
return 'This file has been deleted.';
|
|
593
|
-
}
|
|
594
|
-
if (nodeFile.status === 'renamed' && nodeFile.oldFilepath) {
|
|
595
|
-
return parseRenamedFileDiff(nodeFile, repo, headTree, index, logger);
|
|
596
|
-
}
|
|
597
|
-
// If not deleted or renamed, get the diff from the index
|
|
598
|
-
return parseDefaultFileDiff(nodeFile, repo, headTree, index);
|
|
599
|
-
};
|
|
600
|
-
|
|
601
545
|
// TODO: Extend this to support other models! 🎉
|
|
602
546
|
function getModel(fields, configuration) {
|
|
603
547
|
return new OpenAI(fields, configuration);
|
|
@@ -638,12 +582,50 @@ function validatePromptTemplate(text, inputVariables) {
|
|
|
638
582
|
return true;
|
|
639
583
|
}
|
|
640
584
|
|
|
585
|
+
const parseDefaultFileDiff = async (nodeFile, git) => {
|
|
586
|
+
return await git.diff(['--staged', nodeFile.filepath]);
|
|
587
|
+
};
|
|
588
|
+
const parseRenamedFileDiff = async (nodeFile, git, logger) => {
|
|
589
|
+
let result = '';
|
|
590
|
+
const oldFilepath = nodeFile?.oldFilepath || nodeFile.filepath;
|
|
591
|
+
try {
|
|
592
|
+
const [headContent, indexContent] = await Promise.all([
|
|
593
|
+
git.show([`HEAD:${oldFilepath}`]),
|
|
594
|
+
git.show([`:${nodeFile.filepath}`]),
|
|
595
|
+
]);
|
|
596
|
+
if (headContent !== indexContent) {
|
|
597
|
+
result = createTwoFilesPatch(oldFilepath, nodeFile.filepath, headContent, indexContent, '', '', {
|
|
598
|
+
context: 3,
|
|
599
|
+
});
|
|
600
|
+
// remove the first 4 lines of the patch (they contain the old and new file names)
|
|
601
|
+
result = result.split('\n').slice(4).join('\n');
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
result = 'File contents are unchanged.';
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
catch (err) {
|
|
608
|
+
logger.verbose(`Error comparing file contents for ${nodeFile.filepath}`, { color: 'red' });
|
|
609
|
+
result = 'Error comparing file contents.';
|
|
610
|
+
}
|
|
611
|
+
return result;
|
|
612
|
+
};
|
|
613
|
+
const getDiff = async (nodeFile, { git, logger, }) => {
|
|
614
|
+
if (nodeFile.status === 'deleted') {
|
|
615
|
+
return 'This file has been deleted.';
|
|
616
|
+
}
|
|
617
|
+
if (nodeFile.status === 'renamed' && nodeFile.oldFilepath) {
|
|
618
|
+
const renamedDiff = await parseRenamedFileDiff(nodeFile, git, logger);
|
|
619
|
+
return renamedDiff;
|
|
620
|
+
}
|
|
621
|
+
// If not deleted or renamed, get the diff from the index
|
|
622
|
+
const defaultDiff = await parseDefaultFileDiff(nodeFile, git);
|
|
623
|
+
return defaultDiff;
|
|
624
|
+
};
|
|
625
|
+
|
|
641
626
|
const MAX_TOKENS_PER_SUMMARY = 2048;
|
|
642
|
-
const fileChangeParser = async (changes, { tokenizer,
|
|
627
|
+
const fileChangeParser = async (changes, { tokenizer, git, model }) => {
|
|
643
628
|
const logger = new Logger(config);
|
|
644
|
-
const head = await repo.getHeadCommit();
|
|
645
|
-
const headTree = await head.getTree();
|
|
646
|
-
const index = await repo.refreshIndex();
|
|
647
629
|
const textSplitter = getTextSplitter({ chunkSize: 2000, chunkOverlap: 125, });
|
|
648
630
|
const summarizationChain = getChain(model, {
|
|
649
631
|
type: 'map_reduce',
|
|
@@ -655,7 +637,7 @@ const fileChangeParser = async (changes, { tokenizer, repo, model }) => {
|
|
|
655
637
|
logger.stopTimer('Created file hierarchy');
|
|
656
638
|
// Collect diffs
|
|
657
639
|
logger.startTimer().startSpinner(`Collecting Diffs...\n`, { color: 'blue' });
|
|
658
|
-
const diffs = await collectDiffs(rootTreeNode, (path) =>
|
|
640
|
+
const diffs = await collectDiffs(rootTreeNode, (path) => getDiff(path, { git, logger }), tokenizer, logger);
|
|
659
641
|
logger.stopSpinner('Diffs Collected').stopTimer();
|
|
660
642
|
// Summarize diffs
|
|
661
643
|
logger.startTimer();
|
|
@@ -699,197 +681,102 @@ const getTokenizer = () => {
|
|
|
699
681
|
return tokenizer;
|
|
700
682
|
};
|
|
701
683
|
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
if (patch.isAdded()) {
|
|
709
|
-
summary = `added: ${newFilePath}`;
|
|
710
|
-
}
|
|
711
|
-
else if (patch.isDeleted()) {
|
|
712
|
-
summary = `deleted: ${oldFilePath}`;
|
|
713
|
-
}
|
|
714
|
-
else if (patch.isModified()) {
|
|
715
|
-
summary = `modified: ${newFilePath}`;
|
|
716
|
-
}
|
|
717
|
-
else if (patch.isRenamed()) {
|
|
718
|
-
summary = `renamed: ${oldFilePath} -> ${newFilePath}`;
|
|
719
|
-
}
|
|
720
|
-
else if (patch.isUntracked()) {
|
|
721
|
-
summary = `untracked: ${newFilePath}`;
|
|
722
|
-
}
|
|
723
|
-
else {
|
|
724
|
-
summary = `unknown: ${newFilePath}`;
|
|
725
|
-
}
|
|
726
|
-
return summary;
|
|
684
|
+
const llm = async ({ llm, prompt, variables }) => {
|
|
685
|
+
const chain = new LLMChain({ llm, prompt });
|
|
686
|
+
const res = await chain.call(variables);
|
|
687
|
+
if (res.error)
|
|
688
|
+
throw new Error(res.error);
|
|
689
|
+
return res.text.trim();
|
|
727
690
|
};
|
|
728
691
|
|
|
729
|
-
const getStatus = (
|
|
692
|
+
const getStatus = (file, location = 'index') => {
|
|
693
|
+
const statusCode = file[location] ? file[location] : file.index;
|
|
730
694
|
let status;
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
status = 'unknown';
|
|
695
|
+
switch (statusCode) {
|
|
696
|
+
case 'A':
|
|
697
|
+
status = 'added';
|
|
698
|
+
break;
|
|
699
|
+
case 'D':
|
|
700
|
+
status = 'deleted';
|
|
701
|
+
break;
|
|
702
|
+
case 'M':
|
|
703
|
+
status = 'modified';
|
|
704
|
+
break;
|
|
705
|
+
case 'R':
|
|
706
|
+
status = 'renamed';
|
|
707
|
+
break;
|
|
708
|
+
case '?':
|
|
709
|
+
status = 'untracked';
|
|
710
|
+
break;
|
|
711
|
+
default:
|
|
712
|
+
status = 'unknown';
|
|
713
|
+
break;
|
|
751
714
|
}
|
|
752
715
|
return status;
|
|
753
716
|
};
|
|
754
717
|
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
...(config?.ignoredExtensions?.length && config?.ignoredExtensions?.length > 0
|
|
760
|
-
? config.ignoredExtensions
|
|
761
|
-
: []),
|
|
762
|
-
];
|
|
763
|
-
/**
|
|
764
|
-
* Parse patches from a git diff.
|
|
765
|
-
*
|
|
766
|
-
* @param {ConvenientPatch[]} patches - An array of git patches.
|
|
767
|
-
* @param {string[]} [options.ignoredFiles] - An optional array of file patterns to ignore.
|
|
768
|
-
* If not provided, it defaults to the `ignoredFiles` configuration value from the app's config.
|
|
769
|
-
* @param {string[]} [options.ignoredExtensions] - An optional array of file extensions to ignore.
|
|
770
|
-
* If not provided, it defaults to the `ignoredExtensions` configuration value from the app's config.
|
|
771
|
-
* @returns {Promise<FileChange[]>} A Promise that resolves to an array of file changes.
|
|
772
|
-
**/
|
|
773
|
-
const parsePatches = async (patches, { ignoredFiles = DEFAULT_IGNORED_FILES$1, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS$1, }) => patches
|
|
774
|
-
.map((patch) => {
|
|
775
|
-
const summary = getSummaryText(patch);
|
|
776
|
-
const status = getStatus(patch);
|
|
777
|
-
return {
|
|
778
|
-
filepath: patch.newFile().path(),
|
|
779
|
-
oldFilepath: status === 'renamed' ? patch.oldFile().path() : undefined,
|
|
780
|
-
summary,
|
|
781
|
-
status,
|
|
782
|
-
};
|
|
783
|
-
})
|
|
784
|
-
.filter(Boolean)
|
|
785
|
-
// Filter out ignored files & extensions...
|
|
786
|
-
.filter(({ filepath }) => {
|
|
787
|
-
if (!filepath)
|
|
788
|
-
return false;
|
|
789
|
-
const extension = filepath.split('.').pop();
|
|
790
|
-
// Remove ignored extensions
|
|
791
|
-
if (extension && ignoredExtensions.includes(extension))
|
|
792
|
-
return false;
|
|
793
|
-
// Remove ignored files
|
|
794
|
-
if (ignoredFiles.some((pattern) => minimatch(filepath, pattern)))
|
|
795
|
-
return false;
|
|
796
|
-
return true;
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
const DEFAULT_IGNORED_FILES = [
|
|
800
|
-
...(config?.ignoredFiles?.length && config?.ignoredFiles?.length > 0 ? config.ignoredFiles : []),
|
|
801
|
-
];
|
|
802
|
-
const DEFAULT_IGNORED_EXTENSIONS = [
|
|
803
|
-
...(config?.ignoredExtensions?.length && config?.ignoredExtensions?.length > 0
|
|
804
|
-
? config.ignoredExtensions
|
|
805
|
-
: []),
|
|
806
|
-
];
|
|
807
|
-
/**
|
|
808
|
-
* The 'git status' for coco
|
|
809
|
-
*
|
|
810
|
-
* Get paths of changed files in the Git repository, excluding ignored files and extensions.
|
|
811
|
-
*
|
|
812
|
-
* @param {string[]} [options.ignoredFiles] - An optional array of file patterns to ignore.
|
|
813
|
-
* If not provided, it defaults to the `ignoredFiles` configuration value from the app's config.
|
|
814
|
-
* @param {string[]} [options.ignoredExtensions] - An optional array of file extensions to ignore.
|
|
815
|
-
* If not provided, it defaults to the `ignoredExtensions` configuration value from the app's config.
|
|
816
|
-
* @returns {Promise<GetChangesResult>} A Promise that resolves to an array of changed file paths.
|
|
817
|
-
*
|
|
818
|
-
* @example
|
|
819
|
-
* const changes = await getStagedChanges()
|
|
820
|
-
* console.log(changes)
|
|
821
|
-
* // {
|
|
822
|
-
* // staged: [
|
|
823
|
-
* // {
|
|
824
|
-
* // filepath: 'src/index.ts',
|
|
825
|
-
* // action: 'modified'
|
|
826
|
-
* // },
|
|
827
|
-
* // ],
|
|
828
|
-
* // unstaged: [
|
|
829
|
-
* // {
|
|
830
|
-
* // filepath: 'src/index.test.ts',
|
|
831
|
-
* // action: 'added'
|
|
832
|
-
* // }
|
|
833
|
-
* // ]
|
|
834
|
-
* // }
|
|
835
|
-
*/
|
|
836
|
-
async function getChanges(repo, options = {}) {
|
|
837
|
-
const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS, ignoreUnstaged, ignoreUntracked, } = options;
|
|
838
|
-
const head = await repo.getHeadCommit();
|
|
839
|
-
const index = await repo.refreshIndex();
|
|
840
|
-
const tree = await (head ? await head.getTree() : Tree.lookup(repo, EMPTY_GIT_TREE_HASH));
|
|
841
|
-
let unstaged = [];
|
|
842
|
-
let untracked = [];
|
|
843
|
-
if (!ignoreUnstaged) {
|
|
844
|
-
const unstagedDiff = await Diff.indexToWorkdir(repo, index, {
|
|
845
|
-
flags: 16 /* Diff.OPTION.RECURSE_UNTRACKED_DIRS */,
|
|
846
|
-
});
|
|
847
|
-
const unstagedPatches = await unstagedDiff.patches();
|
|
848
|
-
unstaged = await parsePatches(unstagedPatches, { ignoredFiles, ignoredExtensions });
|
|
849
|
-
}
|
|
850
|
-
if (!ignoreUntracked) {
|
|
851
|
-
const untrackedDiff = await Diff.treeToWorkdirWithIndex(repo, tree, {
|
|
852
|
-
flags: 33554432 /* Diff.OPTION.SHOW_UNTRACKED_CONTENT */,
|
|
853
|
-
});
|
|
854
|
-
const untrackedPatches = await untrackedDiff.patches();
|
|
855
|
-
untracked = (await parsePatches(untrackedPatches, { ignoredFiles, ignoredExtensions })).filter(({ status }) => status === 'untracked');
|
|
718
|
+
const getSummaryText = (file, change) => {
|
|
719
|
+
const status = change.status || getStatus(file);
|
|
720
|
+
if (change.oldFilepath) {
|
|
721
|
+
return `${status}: ${change.oldFilepath} -> ${file.path}`;
|
|
856
722
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
723
|
+
return `${status}: ${file.path}`;
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
const DEFAULT_IGNORED_FILES = config?.ignoredFiles?.length ? config.ignoredFiles : [];
|
|
727
|
+
const DEFAULT_IGNORED_EXTENSIONS = config?.ignoredExtensions?.length ? config.ignoredExtensions : [];
|
|
728
|
+
async function getChanges(git, options = {}) {
|
|
729
|
+
const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options;
|
|
730
|
+
const staged = [];
|
|
731
|
+
const unstaged = [];
|
|
732
|
+
const untracked = [];
|
|
733
|
+
const status = await git.status();
|
|
734
|
+
status.files.forEach((file) => {
|
|
735
|
+
const fileChange = {
|
|
736
|
+
filepath: file.path,
|
|
737
|
+
oldFilepath: status.renamed.filter((renamed) => renamed.to === file.path)[0]?.from,
|
|
738
|
+
};
|
|
739
|
+
// Unstaged files
|
|
740
|
+
if (file.working_dir !== '?' && file.working_dir !== ' ') {
|
|
741
|
+
fileChange.status = getStatus(file, 'working_dir');
|
|
742
|
+
fileChange.summary = getSummaryText(file, fileChange);
|
|
743
|
+
unstaged.push(fileChange);
|
|
744
|
+
}
|
|
745
|
+
// Staged files
|
|
746
|
+
if (file.index !== ' ' && file.index !== '?') {
|
|
747
|
+
fileChange.status = getStatus(file);
|
|
748
|
+
fileChange.summary = getSummaryText(file, fileChange);
|
|
749
|
+
staged.push(fileChange);
|
|
750
|
+
}
|
|
751
|
+
// Untracked files
|
|
752
|
+
if (file.working_dir === '?' && file.index === '?') {
|
|
753
|
+
fileChange.status = 'added';
|
|
754
|
+
fileChange.summary = getSummaryText(file, fileChange);
|
|
755
|
+
untracked.push(fileChange);
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
const ignoredExtensionsSet = new Set(ignoredExtensions.map((extension) => extension.toLowerCase()));
|
|
759
|
+
const filteredStaged = staged.filter((file) => {
|
|
760
|
+
const extension = path__default.extname(file.filepath).toLowerCase();
|
|
761
|
+
return !ignoredExtensionsSet.has(extension) && !ignoredFiles.some(ignoredPattern => minimatch(file.filepath, ignoredPattern));
|
|
762
|
+
});
|
|
763
|
+
const filteredUnstaged = unstaged.filter((file) => {
|
|
764
|
+
const extension = path__default.extname(file.filepath).toLowerCase();
|
|
765
|
+
return !ignoredExtensionsSet.has(extension) && !ignoredFiles.some(ignoredPattern => minimatch(file.filepath, ignoredPattern));
|
|
766
|
+
});
|
|
767
|
+
const filteredUntracked = untracked.filter((file) => {
|
|
768
|
+
const extension = path__default.extname(file.filepath).toLowerCase();
|
|
769
|
+
return !ignoredExtensionsSet.has(extension) && !ignoredFiles.some(ignoredPattern => minimatch(file.filepath, ignoredPattern));
|
|
860
770
|
});
|
|
861
|
-
const patches = await diff.patches();
|
|
862
771
|
return {
|
|
863
|
-
staged:
|
|
864
|
-
unstaged,
|
|
865
|
-
untracked,
|
|
772
|
+
staged: filteredStaged,
|
|
773
|
+
unstaged: filteredUnstaged,
|
|
774
|
+
untracked: filteredUntracked,
|
|
866
775
|
};
|
|
867
776
|
}
|
|
868
777
|
|
|
869
|
-
async
|
|
870
|
-
const
|
|
871
|
-
const index = await repo.refreshIndex();
|
|
872
|
-
await index.addAll();
|
|
873
|
-
await index.write();
|
|
874
|
-
const oid = await index.writeTree();
|
|
875
|
-
const head = await nodegit.Reference.nameToId(repo, "HEAD");
|
|
876
|
-
const parent = await repo.getCommit(head);
|
|
877
|
-
return await repo.createCommit("HEAD", author, author, commitMsg, oid, [parent]);
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
const llm = async ({ llm, prompt, variables }) => {
|
|
881
|
-
const chain = new LLMChain({ llm, prompt });
|
|
882
|
-
const res = await chain.call(variables);
|
|
883
|
-
if (res.error)
|
|
884
|
-
throw new Error(res.error);
|
|
885
|
-
return res.text.trim();
|
|
886
|
-
};
|
|
887
|
-
|
|
888
|
-
const noResult = async ({ repo, logger }) => {
|
|
889
|
-
const { staged, unstaged, untracked } = await getChanges(repo, {
|
|
890
|
-
ignoreUnstaged: false,
|
|
891
|
-
ignoreUntracked: false,
|
|
892
|
-
});
|
|
778
|
+
const noResult = async ({ git, logger }) => {
|
|
779
|
+
const { staged, unstaged, untracked } = await getChanges(git);
|
|
893
780
|
if (staged.length > 0) {
|
|
894
781
|
logger.log(`Staged files detected, but no summary generated...`, { color: 'red' });
|
|
895
782
|
logger.log(`Files are likely either:\n • changed files are ignored\n • file diff is too large.`, { color: 'yellow' });
|
|
@@ -912,25 +799,26 @@ const noResult = async ({ repo, logger }) => {
|
|
|
912
799
|
process.exit(0);
|
|
913
800
|
};
|
|
914
801
|
|
|
802
|
+
async function createCommit(commitMsg, git) {
|
|
803
|
+
return await git.commit(commitMsg);
|
|
804
|
+
}
|
|
805
|
+
|
|
915
806
|
const argv = loadArgv();
|
|
916
807
|
const tokenizer = getTokenizer();
|
|
808
|
+
const git = simpleGit();
|
|
917
809
|
async function main(options) {
|
|
918
810
|
const logger = new Logger(config);
|
|
919
811
|
if (!config.openAIApiKey) {
|
|
920
812
|
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
921
813
|
process.exit(1);
|
|
922
814
|
}
|
|
923
|
-
const repo = await Repository.open('.');
|
|
924
815
|
const model = getModel({
|
|
925
816
|
temperature: 0.4,
|
|
926
817
|
maxConcurrency: 10,
|
|
927
818
|
openAIApiKey: config.openAIApiKey,
|
|
928
819
|
});
|
|
929
820
|
const INTERACTIVE = config?.mode === 'interactive' || options.interactive;
|
|
930
|
-
const { staged: changes } = await getChanges(
|
|
931
|
-
ignoreUnstaged: true,
|
|
932
|
-
ignoreUntracked: true,
|
|
933
|
-
});
|
|
821
|
+
const { staged: changes } = await getChanges(git);
|
|
934
822
|
let summary = '';
|
|
935
823
|
let commitMsg = '';
|
|
936
824
|
let promptTemplate = config?.prompt || '';
|
|
@@ -940,11 +828,11 @@ async function main(options) {
|
|
|
940
828
|
logger.verbose(`\nChanged Files: \n ${changes.map(({ summary }) => summary).join('\n ')}`, {
|
|
941
829
|
color: 'blue',
|
|
942
830
|
});
|
|
943
|
-
summary = await fileChangeParser(changes, { tokenizer,
|
|
831
|
+
summary = await fileChangeParser(changes, { tokenizer, git, model });
|
|
944
832
|
}
|
|
945
833
|
// Handle empty summary
|
|
946
834
|
if (!summary.length) {
|
|
947
|
-
noResult({
|
|
835
|
+
noResult({ git, logger });
|
|
948
836
|
}
|
|
949
837
|
// Prompt user for commit template prompt, if necessary
|
|
950
838
|
if (modifyPrompt) {
|
|
@@ -1059,7 +947,7 @@ async function main(options) {
|
|
|
1059
947
|
// Handle resulting commit message
|
|
1060
948
|
switch (MODE) {
|
|
1061
949
|
case 'interactive':
|
|
1062
|
-
await createCommit(commitMsg,
|
|
950
|
+
await createCommit(commitMsg, git);
|
|
1063
951
|
logSuccess();
|
|
1064
952
|
break;
|
|
1065
953
|
case 'stdout':
|