git-coco 0.4.0 → 0.5.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.
@@ -19,8 +19,8 @@ import { minimatch } from 'minimatch';
19
19
  import * as fs from 'fs';
20
20
  import * as os from 'os';
21
21
  import * as ini from 'ini';
22
- import { simpleGit } from 'simple-git';
23
22
  import { editor, select } from '@inquirer/prompts';
23
+ import { simpleGit } from 'simple-git';
24
24
 
25
25
  /**
26
26
  * Extract the path from a file path string.
@@ -606,6 +606,7 @@ function loadEnvConfig(config) {
606
606
  ignoredExtensions: process.env.COCO_IGNORED_EXTENSIONS
607
607
  ? process.env.COCO_IGNORED_EXTENSIONS.split(',')
608
608
  : undefined,
609
+ defaultBranch: process.env.COCO_DEFAULT_BRANCH || undefined,
609
610
  };
610
611
  config = { ...config, ...removeUndefined(envConfig) };
611
612
  return config;
@@ -634,6 +635,7 @@ function loadGitConfig(config) {
634
635
  summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
635
636
  ignoredFiles: gitConfigParsed.coco?.ignoredFiles || config.ignoredFiles,
636
637
  ignoredExtensions: gitConfigParsed.coco?.ignoredExtensions || config.ignoredExtensions,
638
+ defaultBranch: gitConfigParsed.coco?.defaultBranch || config.defaultBranch,
637
639
  };
638
640
  }
639
641
  return config;
@@ -679,6 +681,8 @@ function loadIgnore(config) {
679
681
  * @returns {Config} Updated config
680
682
  **/
681
683
  function loadProjectConfig(config) {
684
+ // TODO: Add validation based of JSON schema?
685
+ // @see https://github.com/acornejo/jjv
682
686
  if (fs.existsSync('.coco.config.json')) {
683
687
  const projectConfig = JSON.parse(fs.readFileSync('.coco.config.json', 'utf-8'));
684
688
  config = { ...config, ...projectConfig };
@@ -716,6 +720,7 @@ const DEFAULT_CONFIG = {
716
720
  mode: 'stdout',
717
721
  ignoredFiles: ['package-lock.json'],
718
722
  ignoredExtensions: ['.map', '.lock'],
723
+ defaultBranch: 'main',
719
724
  };
720
725
  /**
721
726
  * Load application config
@@ -1016,9 +1021,21 @@ const handleResult = async (result, { mode, git }) => {
1016
1021
  process.exit(0);
1017
1022
  };
1018
1023
 
1019
- const tokenizer = getTokenizer();
1020
- const git$1 = simpleGit();
1024
+ const getRepo = () => {
1025
+ let git;
1026
+ try {
1027
+ git = simpleGit();
1028
+ }
1029
+ catch (e) {
1030
+ console.log('Error initializing git repo', e);
1031
+ process.exit(1);
1032
+ }
1033
+ return git;
1034
+ };
1035
+
1021
1036
  async function handler$1(argv) {
1037
+ const tokenizer = getTokenizer();
1038
+ const git = getRepo();
1022
1039
  const options = loadConfig(argv);
1023
1040
  const logger = new Logger(options);
1024
1041
  const key = getApiKeyForModel(options.model, options);
@@ -1032,14 +1049,14 @@ async function handler$1(argv) {
1032
1049
  });
1033
1050
  const INTERACTIVE = isInteractive(options);
1034
1051
  async function factory() {
1035
- const changes = await getChanges({ git: git$1 });
1052
+ const changes = await getChanges({ git });
1036
1053
  return changes.staged;
1037
1054
  }
1038
1055
  async function parser(changes) {
1039
1056
  return await fileChangeParser({
1040
1057
  changes,
1041
1058
  commit: '--staged',
1042
- options: { tokenizer, git: git$1, model, logger },
1059
+ options: { tokenizer, git, model, logger },
1043
1060
  });
1044
1061
  }
1045
1062
  const commitMsg = await generateAndReviewLoop({
@@ -1058,7 +1075,7 @@ async function handler$1(argv) {
1058
1075
  });
1059
1076
  },
1060
1077
  noResult: async () => {
1061
- await noResult({ git: git$1, logger });
1078
+ await noResult({ git, logger });
1062
1079
  process.exit(0);
1063
1080
  },
1064
1081
  options: {
@@ -1071,7 +1088,7 @@ async function handler$1(argv) {
1071
1088
  const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
1072
1089
  handleResult(commitMsg, {
1073
1090
  mode: MODE,
1074
- git: git$1,
1091
+ git,
1075
1092
  });
1076
1093
  }
1077
1094
 
@@ -1152,15 +1169,9 @@ const CHANGELOG_PROMPT = new PromptTemplate({
1152
1169
 
1153
1170
  async function getCommitLogRange(from, to, { noMerges, git }) {
1154
1171
  try {
1155
- const output = await git.raw([
1156
- 'log',
1157
- `${from}..${to}`,
1158
- '--pretty=format:%s',
1159
- // Include '--no-merges' here if you want to exclude merge commits.
1160
- noMerges ? '--no-merges' : null,
1161
- ].filter(Boolean)); // filter(Boolean) removes any null values from the array
1162
- const messages = output.split('\n').filter(Boolean);
1163
- return messages;
1172
+ const logOptions = { from: `${from}^1`, to, '--no-merges': noMerges };
1173
+ const commitLog = await git.log(logOptions);
1174
+ return commitLog.all.map(({ message, date, body, author_name }) => `[${date}] ${message}\n${body}\n - ${author_name}`);
1164
1175
  }
1165
1176
  catch (error) {
1166
1177
  // If there's an error, handle it appropriately
@@ -1169,10 +1180,56 @@ async function getCommitLogRange(from, to, { noMerges, git }) {
1169
1180
  }
1170
1181
  }
1171
1182
 
1172
- const git = simpleGit();
1183
+ async function getCurrentBranchName({ git }) {
1184
+ return await git.revparse(['--abbrev-ref', 'HEAD']);
1185
+ }
1186
+
1187
+ async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main', comparisonRemote = 'origin', }) {
1188
+ try {
1189
+ // Get the current branch name
1190
+ const branch = await getCurrentBranchName({ git });
1191
+ // Check if the current branch has any commits
1192
+ const hasCommits = (await git.raw(['rev-list', '--count', branch])) !== '0';
1193
+ if (!hasCommits) {
1194
+ logger?.log('No commits on the current branch.');
1195
+ return [];
1196
+ }
1197
+ // Get the list of commits that are unique to the current branch
1198
+ let uniqueCommits;
1199
+ if (comparisonBranch === branch) {
1200
+ // If the comparison branch is the same as the current branch, we compare against the remote.
1201
+ uniqueCommits = (await git.raw(['rev-list', `${comparisonRemote}/${comparisonBranch}..${branch}`]))
1202
+ .split('\n')
1203
+ .filter(Boolean)
1204
+ .reverse();
1205
+ }
1206
+ else {
1207
+ // Your existing code for different branches
1208
+ uniqueCommits = (await git.raw(['rev-list', `${comparisonBranch}..${branch}`]))
1209
+ .split('\n')
1210
+ .filter(Boolean)
1211
+ .reverse();
1212
+ }
1213
+ logger?.verbose(`Found ${uniqueCommits.length} unique commits on "${branch}"`, { color: 'blue' });
1214
+ const firstCommit = uniqueCommits[0];
1215
+ const lastCommit = uniqueCommits[uniqueCommits.length - 1];
1216
+ if (!firstCommit || !lastCommit) {
1217
+ logger?.log('Unable to determine first and last commit on the current branch', { color: 'yellow' });
1218
+ return [];
1219
+ }
1220
+ // Retrieve commit log with messages
1221
+ return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
1222
+ }
1223
+ catch (error) {
1224
+ logger?.log('Encountered an error getting commit log from current branch', { color: 'red' });
1225
+ }
1226
+ return [];
1227
+ }
1228
+
1173
1229
  async function handler(argv) {
1174
1230
  const options = loadConfig(argv);
1175
1231
  const logger = new Logger(options);
1232
+ const git = getRepo();
1176
1233
  const key = getApiKeyForModel(options.model, options);
1177
1234
  if (!key) {
1178
1235
  logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
@@ -1183,14 +1240,17 @@ async function handler(argv) {
1183
1240
  maxConcurrency: 10,
1184
1241
  });
1185
1242
  const INTERACTIVE = isInteractive(options);
1186
- const [from, to] = options.range?.split(':');
1187
- if (!from || !to) {
1188
- logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
1189
- process.exit(1);
1190
- }
1191
1243
  async function factory() {
1192
- const messages = await getCommitLogRange(from, to, { git, noMerges: true });
1193
- return messages;
1244
+ if (options.range) {
1245
+ const [from, to] = options.range?.split(':');
1246
+ if (!from || !to) {
1247
+ logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
1248
+ process.exit(1);
1249
+ }
1250
+ return await getCommitLogRange(from, to, { git, noMerges: true });
1251
+ }
1252
+ logger.verbose(`No range provided. Defaulting to current branch`, { color: 'yellow' });
1253
+ return await getCommitLogCurrentBranch({ git, logger });
1194
1254
  }
1195
1255
  async function parser(messages) {
1196
1256
  const result = messages.join('\n');
@@ -1213,7 +1273,11 @@ async function handler(argv) {
1213
1273
  });
1214
1274
  },
1215
1275
  noResult: async () => {
1216
- await noResult({ git, logger });
1276
+ if (options.range) {
1277
+ logger.log(`No commits found in the provided range.`, { color: 'red' });
1278
+ process.exit(0);
1279
+ }
1280
+ logger.log(`No commits found in the current branch.`, { color: 'red' });
1217
1281
  process.exit(0);
1218
1282
  },
1219
1283
  options: {
@@ -1238,7 +1302,6 @@ const options = {
1238
1302
  type: 'string',
1239
1303
  alias: 'r',
1240
1304
  description: 'Commit range e.g `HEAD~2:HEAD`',
1241
- demandOption: true,
1242
1305
  },
1243
1306
  model: { type: 'string', description: 'LLM/Model-Name' },
1244
1307
  openAIApiKey: {