git-coco 0.21.4 → 0.22.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/dist/index.d.ts CHANGED
@@ -173,6 +173,10 @@ interface ChangelogOptions extends BaseCommandOptions {
173
173
  range: string;
174
174
  branch: string;
175
175
  sinceLastTag: boolean;
176
+ withDiff?: boolean;
177
+ onlyDiff?: boolean;
178
+ additional?: string;
179
+ author?: boolean;
176
180
  }
177
181
  type ChangelogArgv = Arguments<ChangelogOptions>;
178
182
 
@@ -48,7 +48,7 @@ import { pathToFileURL } from 'url';
48
48
  /**
49
49
  * Current build version from package.json
50
50
  */
51
- const BUILD_VERSION = "0.21.4";
51
+ const BUILD_VERSION = "0.22.0";
52
52
 
53
53
  const isInteractive = (config) => {
54
54
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -1018,6 +1018,15 @@ const schema$1 = {
1018
1018
  "apiKey": {
1019
1019
  "type": "string",
1020
1020
  "description": "API key to use when making requests to OpenAI. Defaults to the value of `OPENAI_API_KEY` environment variable."
1021
+ },
1022
+ "verbosity": {
1023
+ "type": "string",
1024
+ "enum": [
1025
+ "low",
1026
+ "medium",
1027
+ "high"
1028
+ ],
1029
+ "description": "The verbosity of the model's response."
1021
1030
  }
1022
1031
  }
1023
1032
  },
@@ -6054,6 +6063,26 @@ const options$4 = {
6054
6063
  description: 'Generate changelog for all commits since the last tag',
6055
6064
  default: false,
6056
6065
  },
6066
+ withDiff: {
6067
+ type: 'boolean',
6068
+ description: 'Include the diff for each commit in the prompt',
6069
+ default: false,
6070
+ },
6071
+ onlyDiff: {
6072
+ type: 'boolean',
6073
+ description: 'Generate a changelog based only on the diff of the entire branch',
6074
+ default: false,
6075
+ },
6076
+ additional: {
6077
+ type: 'string',
6078
+ alias: 'a',
6079
+ description: 'Add extra contextual information to the prompt',
6080
+ },
6081
+ author: {
6082
+ type: 'boolean',
6083
+ description: 'Include author attribution in the changelog',
6084
+ default: false,
6085
+ },
6057
6086
  i: {
6058
6087
  type: 'boolean',
6059
6088
  alias: 'interactive',
@@ -6958,45 +6987,39 @@ const getChangesSinceLastTag = async ({ git }) => {
6958
6987
  };
6959
6988
 
6960
6989
  /**
6961
- * Retrieves the commit log range between two specified commits (inclusive of both commits).
6990
+ * Retrieves the detailed commit log range between two specified commits (inclusive of both commits).
6962
6991
  *
6963
6992
  * @param from - The starting commit (can be a commit hash, HEAD reference, or branch name). This commit will be included in the results.
6964
6993
  * @param to - The ending commit (can be a commit hash, HEAD reference, or branch name). This commit will be included in the results.
6965
6994
  * @param options - Additional options for retrieving the commit log range.
6966
- * @returns A promise that resolves to an array of commit log messages, including both the 'from' and 'to' commits.
6995
+ * @returns A promise that resolves to an array of commit details objects.
6967
6996
  * @throws If there is an error retrieving the commit log range.
6968
6997
  */
6969
- async function getCommitLogRange(from, to, { noMerges, git }) {
6998
+ async function getCommitLogRangeDetails(from, to, { noMerges, git }) {
6970
6999
  try {
6971
- // Use from^..to to include the 'from' commit in the range
6972
- // This works because from^..to means "commits reachable from 'to' but not from the parent of 'from'"
6973
7000
  const logOptions = {
6974
7001
  from: `${from}^`,
6975
7002
  to,
6976
7003
  '--no-merges': noMerges
6977
7004
  };
6978
7005
  const commitLog = await git.log(logOptions);
6979
- return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
7006
+ return [...commitLog.all];
6980
7007
  }
6981
7008
  catch (error) {
6982
- // If from^ fails (e.g., 'from' is the first commit), fall back to using from..to and manually adding the 'from' commit
6983
7009
  if (error instanceof Error && error.message.includes('unknown revision')) {
6984
7010
  try {
6985
- // Get the 'from' commit separately
6986
7011
  const fromCommitLog = await git.log({ from: from, maxCount: 1 });
6987
7012
  const fromCommit = fromCommitLog.latest;
6988
- // Get the range from..to (excluding 'from')
6989
7013
  const rangeLogOptions = {
6990
7014
  from,
6991
7015
  to,
6992
7016
  '--no-merges': noMerges
6993
7017
  };
6994
7018
  const rangeCommitLog = await git.log(rangeLogOptions);
6995
- // Combine the 'from' commit with the range commits
6996
7019
  const allCommits = fromCommit
6997
7020
  ? [fromCommit, ...rangeCommitLog.all]
6998
- : rangeCommitLog.all;
6999
- return allCommits.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
7021
+ : [...rangeCommitLog.all];
7022
+ return allCommits;
7000
7023
  }
7001
7024
  catch (fallbackError) {
7002
7025
  throw fallbackError;
@@ -7023,7 +7046,7 @@ async function getCurrentBranchName({ git }) {
7023
7046
  * @param {SimpleGit} options.git - The SimpleGit instance.
7024
7047
  * @param {Logger} options.logger - The logger for logging messages.
7025
7048
  * @param {string} options.targetBranch - The target branch to compare against.
7026
- * @returns {Promise<string[]>} The array of commit messages in the commit log.
7049
+ * @returns {Promise<CommitDetails[]>} The array of commit messages in the commit log.
7027
7050
  */
7028
7051
  async function getCommitLogAgainstBranch({ git, logger, targetBranch, }) {
7029
7052
  try {
@@ -7042,7 +7065,7 @@ async function getCommitLogAgainstBranch({ git, logger, targetBranch, }) {
7042
7065
  return [];
7043
7066
  }
7044
7067
  // Retrieve commit log with messages
7045
- return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
7068
+ return await getCommitLogRangeDetails(firstCommit, lastCommit, { git, noMerges: true });
7046
7069
  }
7047
7070
  catch (error) {
7048
7071
  logger?.log('Encountered an error getting commit log between branches', { color: 'red' });
@@ -7058,7 +7081,7 @@ async function getCommitLogAgainstBranch({ git, logger, targetBranch, }) {
7058
7081
  * @param {Logger} options.logger - The logger for logging messages.
7059
7082
  * @param {string} [options.comparisonBranch='main'] - The branch to compare against.
7060
7083
  * @param {string} [options.comparisonRemote='origin'] - The remote to compare against.
7061
- * @returns {Promise<string[]>} The array of commit messages in the commit log.
7084
+ * @returns {Promise<CommitDetails[]>} The array of commit messages in the commit log.
7062
7085
  */
7063
7086
  async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main', comparisonRemote = 'origin', }) {
7064
7087
  try {
@@ -7094,7 +7117,7 @@ async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main
7094
7117
  });
7095
7118
  return [];
7096
7119
  }
7097
- return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
7120
+ return await getCommitLogRangeDetails(firstCommit, lastCommit, { git, noMerges: true });
7098
7121
  }
7099
7122
  catch (error) {
7100
7123
  logger?.log('Encountered an error getting commit log from current branch', { color: 'red' });
@@ -7460,18 +7483,202 @@ async function handleResult({ result, mode, interactiveModeCallback }) {
7460
7483
  }
7461
7484
  }
7462
7485
 
7463
- const template$3 = `Write informative git changelog, in the imperative, based on a series of individual messages.
7486
+ /**
7487
+ * Fetches the diff for the given commit ID.
7488
+ *
7489
+ * @param commitId The commit ID for which the diff is to be retrieved.
7490
+ * @returns A promise that resolves to the diff of the commit.
7491
+ */
7492
+ async function getDiffForCommit(commitId, { git, }) {
7493
+ try {
7494
+ return await git.diff(['-p', `${commitId}^..${commitId}`]);
7495
+ }
7496
+ catch (error) {
7497
+ throw new Error(`Error fetching diff for commit ${commitId}: ${error.message}`);
7498
+ }
7499
+ }
7464
7500
 
7465
- - Annotate each change with the git commit hash as reference, including just the first 7 characters
7466
- - Logically group changes, and if necessary, summarize dependency updates
7467
- - Include a descriptive title for the changelog, to give a high-level overview of the changes
7468
- - Depending on the size of the changes, consider breaking the changelog into sections
7469
- - Avoid generlizations like "various bug fixes" or "improvements" or "enhancements"
7501
+ /**
7502
+ * Determines the status of a file based on its changes in the Git repository.
7503
+ *
7504
+ * @param file - The file to check the status of.
7505
+ * @param location - The location to check the status in ('index' or 'working_dir'). Defaults to 'index'.
7506
+ * @returns The status of the file ('added', 'deleted', 'modified', 'renamed', 'untracked', or 'unknown').
7507
+ * @throws Error if the file type is invalid.
7508
+ */
7509
+ function getStatus(file, location = 'index') {
7510
+ if ('index' in file && 'working_dir' in file) {
7511
+ const statusCode = file[location];
7512
+ switch (statusCode) {
7513
+ case 'A':
7514
+ return 'added';
7515
+ case 'D':
7516
+ return 'deleted';
7517
+ case 'M':
7518
+ return 'modified';
7519
+ case 'R':
7520
+ return 'renamed';
7521
+ case '?':
7522
+ return 'untracked';
7523
+ default:
7524
+ return 'unknown';
7525
+ }
7526
+ }
7527
+ else if ('changes' in file && 'binary' in file) {
7528
+ if (file.changes === 0)
7529
+ return 'untracked';
7530
+ if (file.file.includes('=>'))
7531
+ return 'renamed';
7532
+ if (file.deletions === 0 && file.insertions > 0)
7533
+ return 'added';
7534
+ if (file.insertions === 0 && file.deletions > 0)
7535
+ return 'deleted';
7536
+ if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
7537
+ return 'modified';
7538
+ return 'unknown';
7539
+ }
7540
+ else {
7541
+ throw new Error('Invalid file type');
7542
+ }
7543
+ }
7470
7544
 
7545
+ /**
7546
+ * Returns the summary text for a file change.
7547
+ *
7548
+ * @param file - The file status or diff result.
7549
+ * @param change - The partial file change object.
7550
+ * @returns The summary text for the file change.
7551
+ * @throws Error if the file type is invalid.
7552
+ */
7553
+ function getSummaryText(file, change) {
7554
+ const status = change.status || getStatus(file);
7555
+ let filePath;
7556
+ if ('path' in file) {
7557
+ filePath = file.path;
7558
+ }
7559
+ else if ('file' in file) {
7560
+ filePath = change?.filePath || file.file;
7561
+ }
7562
+ else {
7563
+ throw new Error('Invalid file type');
7564
+ }
7565
+ if (change.oldFilePath) {
7566
+ return `${status}: ${change.oldFilePath} -> ${filePath}`;
7567
+ }
7568
+ return `${status}: ${filePath}`;
7569
+ }
7570
+
7571
+ /**
7572
+ * Retrieves the diff between the current branch and a specified target branch.
7573
+ *
7574
+ * @param {Object} options - The options for retrieving the diff.
7575
+ * @param {SimpleGit} options.git - The SimpleGit instance.
7576
+ * @param {Logger} options.logger - The logger for logging messages.
7577
+ * @param {string} options.baseBranch - The base branch to compare against.
7578
+ * @param {string} options.headBranch - The head branch to compare.
7579
+ * @param {string[]} options.ignoredFiles - Array of specific files to ignore.
7580
+ * @param {string[]} options.ignoredExtensions - Array of file extensions to ignore.
7581
+ * @returns {Promise<GetChangesResult>} The diff between the current branch and the target branch.
7582
+ */
7583
+ async function getDiffForBranch({ git, logger, baseBranch, headBranch, options, }) {
7584
+ try {
7585
+ logger?.verbose(`Getting diff for branches: baseBranch="${baseBranch}", headBranch="${headBranch}"`, {
7586
+ color: 'blue',
7587
+ });
7588
+ // Validate branch names
7589
+ if (!baseBranch || !headBranch) {
7590
+ throw new Error(`Invalid branch names: baseBranch="${baseBranch}", headBranch="${headBranch}"`);
7591
+ }
7592
+ const { ignoredFiles = [], ignoredExtensions = [] } = options || {};
7593
+ // Prepare ignore patterns
7594
+ const ignorePatterns = [
7595
+ ...ignoredFiles.map((file) => `:!${file}`),
7596
+ ...ignoredExtensions.map((ext) => `:!*${ext}`),
7597
+ ];
7598
+ // Construct the diff command
7599
+ const diffArgs = [`${baseBranch}..${headBranch}`];
7600
+ if (ignorePatterns.length > 0) {
7601
+ diffArgs.push('--');
7602
+ diffArgs.push(...ignorePatterns);
7603
+ }
7604
+ logger?.verbose(`Running git diff with args: ${diffArgs.join(' ')}`, {
7605
+ color: 'blue',
7606
+ });
7607
+ // Get the diff
7608
+ const diff = await git.diff(diffArgs);
7609
+ logger?.verbose(`Generated diff between "${headBranch}" and "${baseBranch}"`, {
7610
+ color: 'blue',
7611
+ });
7612
+ const changes = diff.split('diff --git').slice(1).map((fileDiff) => {
7613
+ const lines = fileDiff.split('\n');
7614
+ const filePathLine = lines[0];
7615
+ const filePath = filePathLine.split('b/')[1]?.split(' ')[0];
7616
+ const oldFilePath = filePathLine.split('a/')[1]?.split(' ')[0];
7617
+ // Determine status based on diff headers
7618
+ let status = 'modified';
7619
+ if (fileDiff.includes('new file mode')) {
7620
+ status = 'added';
7621
+ }
7622
+ else if (fileDiff.includes('deleted file mode')) {
7623
+ status = 'deleted';
7624
+ }
7625
+ else if (fileDiff.includes('rename from')) {
7626
+ status = 'renamed';
7627
+ }
7628
+ return {
7629
+ filePath: filePath || '',
7630
+ oldFilePath: oldFilePath || '',
7631
+ status,
7632
+ summary: getSummaryText({ path: filePath || '', index: '', working_dir: '' }, { filePath: filePath || '', status }),
7633
+ };
7634
+ });
7635
+ return {
7636
+ staged: changes,
7637
+ unstaged: [],
7638
+ untracked: [],
7639
+ };
7640
+ }
7641
+ catch (error) {
7642
+ const errorMessage = error instanceof Error ? error.message : String(error);
7643
+ console.error('Error in getDiffForBranch:', error);
7644
+ logger?.log(`Encountered an error getting diff between branches: ${errorMessage}`, { color: 'red' });
7645
+ logger?.log(`Branch details: baseBranch="${baseBranch}", headBranch="${headBranch}"`, { color: 'red' });
7646
+ // Re-throw the error so the caller can handle it appropriately
7647
+ throw error;
7648
+ }
7649
+ }
7650
+
7651
+ const template$3 = `You are a highly skilled software engineer tasked with writing a git changelog. Your response should be informative, well-structured, and in the imperative.
7652
+
7653
+ ## Input
7654
+ You will be provided with a summary of changes. This summary can be one of the following:
7655
+ 1. A list of commits, each with its author, hash, message, and body.
7656
+ 2. A list of commits, each with its details AND the full diff of the changes.
7657
+ 3. A single, comprehensive diff for an entire branch.
7658
+
7659
+ ## Rules
7660
+ - Create a descriptive title for the changelog that gives a high-level overview of the changes.
7661
+ - **BREAKING CHANGES**: Identify any commits that introduce breaking changes. These must be listed first under a "### 💥 BREAKING CHANGES" heading.
7662
+ - **Grouping**: Logically group related changes under descriptive headings (e.g., ### Features, ### Fixes, ### Refactors).
7663
+ - **Dependencies**: Group all dependency updates (e.g., changes to package.json, go.mod) under a "### Dependencies" section.
7664
+ - **Summaries**: For each change, provide a concise summary.
7665
+ - **Attribution**: {{author_instructions}}
7666
+ - **Technical Details**: If provided with diffs, use them to understand the technical details and provide a more accurate and detailed description of the changes.
7667
+ - **Clarity**: Avoid generalizations like "various bug fixes," "improvements," or "enhancements." Be specific.
7668
+ - **Formatting**: Your entire response must be valid Markdown.
7669
+
7670
+ ## Formatting Instructions
7471
7671
  {{format_instructions}}
7472
7672
 
7673
+ {{additional_context}}
7674
+
7473
7675
  """{{summary}}"""`;
7474
- const inputVariables$2 = ['format_instructions', 'summary'];
7676
+ const inputVariables$2 = [
7677
+ 'format_instructions',
7678
+ 'summary',
7679
+ 'additional_context',
7680
+ 'author_instructions',
7681
+ ];
7475
7682
  const CHANGELOG_PROMPT = new PromptTemplate({
7476
7683
  template: template$3,
7477
7684
  inputVariables: inputVariables$2,
@@ -7495,48 +7702,68 @@ const handler$4 = async (argv, logger) => {
7495
7702
  }
7496
7703
  async function factory() {
7497
7704
  const branchName = await getCurrentBranchName({ git });
7498
- if (config.sinceLastTag) {
7499
- logger.verbose(`Generating commit log since the last tag`, { color: 'yellow' });
7705
+ if (argv.onlyDiff) {
7706
+ logger.verbose(`Generating changelog based on branch diff`, { color: 'yellow' });
7707
+ const diff = await getDiffForBranch({ git, logger, baseBranch: argv.branch || 'main', headBranch: branchName });
7500
7708
  return {
7501
7709
  branch: branchName,
7502
- commits: await getChangesSinceLastTag({ git, logger }),
7710
+ diff: JSON.stringify(diff.staged, null, 2),
7503
7711
  };
7504
7712
  }
7505
- if (config.range && config.range.includes(':')) {
7713
+ let commits = [];
7714
+ if (config.sinceLastTag) {
7715
+ logger.verbose(`Generating commit log since the last tag`, { color: 'yellow' });
7716
+ // This function returns string[], needs to be adapted or replaced
7717
+ // For now, this path will have limited details.
7718
+ const commitMessages = await getChangesSinceLastTag({ git, logger });
7719
+ commits = commitMessages.map(msg => ({ message: msg }));
7720
+ }
7721
+ else if (config.range && config.range.includes(':')) {
7506
7722
  const [from, to] = config.range.split(':');
7507
7723
  if (!from || !to) {
7508
7724
  logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
7509
7725
  process.exit(1);
7510
7726
  }
7511
- return {
7512
- branch: branchName,
7513
- commits: await getCommitLogRange(from, to, { git, noMerges: true }),
7514
- };
7727
+ commits = await getCommitLogRangeDetails(from, to, { git, noMerges: true });
7515
7728
  }
7516
- if (argv.branch) {
7729
+ else if (argv.branch) {
7517
7730
  logger.verbose(`Generating commit log against branch: ${argv.branch}`, { color: 'yellow' });
7518
- return {
7519
- branch: branchName,
7520
- commits: await getCommitLogAgainstBranch({ git, logger, targetBranch: argv.branch }),
7521
- };
7731
+ commits = await getCommitLogAgainstBranch({ git, logger, targetBranch: argv.branch });
7732
+ }
7733
+ else {
7734
+ logger.verbose(`No range, branch, or tag option provided. Defaulting to current branch`, {
7735
+ color: 'yellow',
7736
+ });
7737
+ commits = await getCommitLogCurrentBranch({ git, logger });
7738
+ }
7739
+ let commitsWithDiffText = commits;
7740
+ if (argv.withDiff) {
7741
+ commitsWithDiffText = await Promise.all(commits.map(async (commit) => ({
7742
+ ...commit,
7743
+ diffText: await getDiffForCommit(commit.hash, { git }),
7744
+ })));
7522
7745
  }
7523
- logger.verbose(`No range, branch, or tag option provided. Defaulting to current branch`, {
7524
- color: 'yellow',
7525
- });
7526
- const commits = await getCommitLogCurrentBranch({ git, logger });
7527
7746
  return {
7528
7747
  branch: branchName,
7529
- commits,
7748
+ commits: commitsWithDiffText,
7749
+ withDiff: argv.withDiff,
7530
7750
  };
7531
7751
  }
7532
- async function parser({ branch, commits }) {
7533
- let result;
7534
- if (!commits || commits.length === 0) {
7535
- result = `## ${branch}\n\nNo commits found.`;
7752
+ async function parser(data) {
7753
+ if (data.diff) {
7754
+ return `## Diff for ${data.branch}\n\n${data.diff}`;
7536
7755
  }
7537
- else {
7538
- result = `## ${branch}\n\n${commits.map((commit) => commit.trim()).join('\n\n')}`;
7756
+ if (!data.commits || data.commits.length === 0) {
7757
+ return `## ${data.branch}\n\nNo commits found.`;
7539
7758
  }
7759
+ let result = `## ${data.branch}\n\n`;
7760
+ result += data.commits.map(commit => {
7761
+ let commitStr = `Author: ${commit.author_name}\nCommit: ${commit.hash}\nMessage: ${commit.message}\n${commit.body}`;
7762
+ if (data.withDiff && commit.diffText) {
7763
+ commitStr += `\nDiff:\n${commit.diffText}`;
7764
+ }
7765
+ return commitStr.trim();
7766
+ }).join('\n\n---\n\n');
7540
7767
  return result;
7541
7768
  }
7542
7769
  const changelogMsg = await generateAndReviewLoop({
@@ -7560,12 +7787,21 @@ const handler$4 = async (argv, logger) => {
7560
7787
  fallback: CHANGELOG_PROMPT,
7561
7788
  });
7562
7789
  const formatInstructions = "Only respond with a valid JSON object, containing two fields: 'title' an escaped string, no more than 65 characters, and 'content' also an escaped string.";
7790
+ let additional_context = '';
7791
+ if (argv.additional) {
7792
+ additional_context = `## Additional Context\n${argv.additional}`;
7793
+ }
7794
+ const author_instructions = argv.author
7795
+ ? 'At the end of each item, attribute the author and include a reference to the commit hash, like this: `by @author_name (f6dbe61)`. Use the first 7 characters of the hash.'
7796
+ : 'At the end of each item, include a reference to the commit hash, like this: `(f6dbe61)`. Use the first 7 characters of the hash.';
7563
7797
  const changelog = await executeChain({
7564
7798
  llm,
7565
7799
  prompt,
7566
7800
  variables: {
7567
7801
  summary: context,
7568
7802
  format_instructions: formatInstructions,
7803
+ additional_context: additional_context,
7804
+ author_instructions: author_instructions,
7569
7805
  },
7570
7806
  parser,
7571
7807
  });
@@ -10877,76 +11113,6 @@ async function createCommit(message, git) {
10877
11113
  return await git.commit(message);
10878
11114
  }
10879
11115
 
10880
- /**
10881
- * Determines the status of a file based on its changes in the Git repository.
10882
- *
10883
- * @param file - The file to check the status of.
10884
- * @param location - The location to check the status in ('index' or 'working_dir'). Defaults to 'index'.
10885
- * @returns The status of the file ('added', 'deleted', 'modified', 'renamed', 'untracked', or 'unknown').
10886
- * @throws Error if the file type is invalid.
10887
- */
10888
- function getStatus(file, location = 'index') {
10889
- if ('index' in file && 'working_dir' in file) {
10890
- const statusCode = file[location];
10891
- switch (statusCode) {
10892
- case 'A':
10893
- return 'added';
10894
- case 'D':
10895
- return 'deleted';
10896
- case 'M':
10897
- return 'modified';
10898
- case 'R':
10899
- return 'renamed';
10900
- case '?':
10901
- return 'untracked';
10902
- default:
10903
- return 'unknown';
10904
- }
10905
- }
10906
- else if ('changes' in file && 'binary' in file) {
10907
- if (file.changes === 0)
10908
- return 'untracked';
10909
- if (file.file.includes('=>'))
10910
- return 'renamed';
10911
- if (file.deletions === 0 && file.insertions > 0)
10912
- return 'added';
10913
- if (file.insertions === 0 && file.deletions > 0)
10914
- return 'deleted';
10915
- if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
10916
- return 'modified';
10917
- return 'unknown';
10918
- }
10919
- else {
10920
- throw new Error('Invalid file type');
10921
- }
10922
- }
10923
-
10924
- /**
10925
- * Returns the summary text for a file change.
10926
- *
10927
- * @param file - The file status or diff result.
10928
- * @param change - The partial file change object.
10929
- * @returns The summary text for the file change.
10930
- * @throws Error if the file type is invalid.
10931
- */
10932
- function getSummaryText(file, change) {
10933
- const status = change.status || getStatus(file);
10934
- let filePath;
10935
- if ('path' in file) {
10936
- filePath = file.path;
10937
- }
10938
- else if ('file' in file) {
10939
- filePath = change?.filePath || file.file;
10940
- }
10941
- else {
10942
- throw new Error('Invalid file type');
10943
- }
10944
- if (change.oldFilePath) {
10945
- return `${status}: ${change.oldFilePath} -> ${filePath}`;
10946
- }
10947
- return `${status}: ${filePath}`;
10948
- }
10949
-
10950
11116
  /**
10951
11117
  * Retrieves the changes in the Git repository.
10952
11118
  *
@@ -11984,86 +12150,6 @@ const getChangesByTimestamp = async ({ since, git }) => {
11984
12150
  return formatCommitLog(commitLog);
11985
12151
  };
11986
12152
 
11987
- /**
11988
- * Retrieves the diff between the current branch and a specified target branch.
11989
- *
11990
- * @param {Object} options - The options for retrieving the diff.
11991
- * @param {SimpleGit} options.git - The SimpleGit instance.
11992
- * @param {Logger} options.logger - The logger for logging messages.
11993
- * @param {string} options.baseBranch - The base branch to compare against.
11994
- * @param {string} options.headBranch - The head branch to compare.
11995
- * @param {string[]} options.ignoredFiles - Array of specific files to ignore.
11996
- * @param {string[]} options.ignoredExtensions - Array of file extensions to ignore.
11997
- * @returns {Promise<GetChangesResult>} The diff between the current branch and the target branch.
11998
- */
11999
- async function getDiffForBranch({ git, logger, baseBranch, headBranch, options, }) {
12000
- try {
12001
- logger?.verbose(`Getting diff for branches: baseBranch="${baseBranch}", headBranch="${headBranch}"`, {
12002
- color: 'blue',
12003
- });
12004
- // Validate branch names
12005
- if (!baseBranch || !headBranch) {
12006
- throw new Error(`Invalid branch names: baseBranch="${baseBranch}", headBranch="${headBranch}"`);
12007
- }
12008
- const { ignoredFiles = [], ignoredExtensions = [] } = options || {};
12009
- // Prepare ignore patterns
12010
- const ignorePatterns = [
12011
- ...ignoredFiles.map((file) => `:!${file}`),
12012
- ...ignoredExtensions.map((ext) => `:!*${ext}`),
12013
- ];
12014
- // Construct the diff command
12015
- const diffArgs = [`${baseBranch}..${headBranch}`];
12016
- if (ignorePatterns.length > 0) {
12017
- diffArgs.push('--');
12018
- diffArgs.push(...ignorePatterns);
12019
- }
12020
- logger?.verbose(`Running git diff with args: ${diffArgs.join(' ')}`, {
12021
- color: 'blue',
12022
- });
12023
- // Get the diff
12024
- const diff = await git.diff(diffArgs);
12025
- logger?.verbose(`Generated diff between "${headBranch}" and "${baseBranch}"`, {
12026
- color: 'blue',
12027
- });
12028
- const changes = diff.split('diff --git').slice(1).map((fileDiff) => {
12029
- const lines = fileDiff.split('\n');
12030
- const filePathLine = lines[0];
12031
- const filePath = filePathLine.split('b/')[1]?.split(' ')[0];
12032
- const oldFilePath = filePathLine.split('a/')[1]?.split(' ')[0];
12033
- // Determine status based on diff headers
12034
- let status = 'modified';
12035
- if (fileDiff.includes('new file mode')) {
12036
- status = 'added';
12037
- }
12038
- else if (fileDiff.includes('deleted file mode')) {
12039
- status = 'deleted';
12040
- }
12041
- else if (fileDiff.includes('rename from')) {
12042
- status = 'renamed';
12043
- }
12044
- return {
12045
- filePath: filePath || '',
12046
- oldFilePath: oldFilePath || '',
12047
- status,
12048
- summary: getSummaryText({ path: filePath || '', index: '', working_dir: '' }, { filePath: filePath || '', status }),
12049
- };
12050
- });
12051
- return {
12052
- staged: changes,
12053
- unstaged: [],
12054
- untracked: [],
12055
- };
12056
- }
12057
- catch (error) {
12058
- const errorMessage = error instanceof Error ? error.message : String(error);
12059
- console.error('Error in getDiffForBranch:', error);
12060
- logger?.log(`Encountered an error getting diff between branches: ${errorMessage}`, { color: 'red' });
12061
- logger?.log(`Branch details: baseBranch="${baseBranch}", headBranch="${headBranch}"`, { color: 'red' });
12062
- // Re-throw the error so the caller can handle it appropriately
12063
- throw error;
12064
- }
12065
- }
12066
-
12067
12153
  async function noResult$1({ logger }) {
12068
12154
  logger.log('No repo changes detected. 👀', { color: 'blue' });
12069
12155
  throw new Error('NO_CHANGES_DETECTED');
package/dist/index.js CHANGED
@@ -70,7 +70,7 @@ var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline);
70
70
  /**
71
71
  * Current build version from package.json
72
72
  */
73
- const BUILD_VERSION = "0.21.4";
73
+ const BUILD_VERSION = "0.22.0";
74
74
 
75
75
  const isInteractive = (config) => {
76
76
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -1040,6 +1040,15 @@ const schema$1 = {
1040
1040
  "apiKey": {
1041
1041
  "type": "string",
1042
1042
  "description": "API key to use when making requests to OpenAI. Defaults to the value of `OPENAI_API_KEY` environment variable."
1043
+ },
1044
+ "verbosity": {
1045
+ "type": "string",
1046
+ "enum": [
1047
+ "low",
1048
+ "medium",
1049
+ "high"
1050
+ ],
1051
+ "description": "The verbosity of the model's response."
1043
1052
  }
1044
1053
  }
1045
1054
  },
@@ -6076,6 +6085,26 @@ const options$4 = {
6076
6085
  description: 'Generate changelog for all commits since the last tag',
6077
6086
  default: false,
6078
6087
  },
6088
+ withDiff: {
6089
+ type: 'boolean',
6090
+ description: 'Include the diff for each commit in the prompt',
6091
+ default: false,
6092
+ },
6093
+ onlyDiff: {
6094
+ type: 'boolean',
6095
+ description: 'Generate a changelog based only on the diff of the entire branch',
6096
+ default: false,
6097
+ },
6098
+ additional: {
6099
+ type: 'string',
6100
+ alias: 'a',
6101
+ description: 'Add extra contextual information to the prompt',
6102
+ },
6103
+ author: {
6104
+ type: 'boolean',
6105
+ description: 'Include author attribution in the changelog',
6106
+ default: false,
6107
+ },
6079
6108
  i: {
6080
6109
  type: 'boolean',
6081
6110
  alias: 'interactive',
@@ -6980,45 +7009,39 @@ const getChangesSinceLastTag = async ({ git }) => {
6980
7009
  };
6981
7010
 
6982
7011
  /**
6983
- * Retrieves the commit log range between two specified commits (inclusive of both commits).
7012
+ * Retrieves the detailed commit log range between two specified commits (inclusive of both commits).
6984
7013
  *
6985
7014
  * @param from - The starting commit (can be a commit hash, HEAD reference, or branch name). This commit will be included in the results.
6986
7015
  * @param to - The ending commit (can be a commit hash, HEAD reference, or branch name). This commit will be included in the results.
6987
7016
  * @param options - Additional options for retrieving the commit log range.
6988
- * @returns A promise that resolves to an array of commit log messages, including both the 'from' and 'to' commits.
7017
+ * @returns A promise that resolves to an array of commit details objects.
6989
7018
  * @throws If there is an error retrieving the commit log range.
6990
7019
  */
6991
- async function getCommitLogRange(from, to, { noMerges, git }) {
7020
+ async function getCommitLogRangeDetails(from, to, { noMerges, git }) {
6992
7021
  try {
6993
- // Use from^..to to include the 'from' commit in the range
6994
- // This works because from^..to means "commits reachable from 'to' but not from the parent of 'from'"
6995
7022
  const logOptions = {
6996
7023
  from: `${from}^`,
6997
7024
  to,
6998
7025
  '--no-merges': noMerges
6999
7026
  };
7000
7027
  const commitLog = await git.log(logOptions);
7001
- return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
7028
+ return [...commitLog.all];
7002
7029
  }
7003
7030
  catch (error) {
7004
- // If from^ fails (e.g., 'from' is the first commit), fall back to using from..to and manually adding the 'from' commit
7005
7031
  if (error instanceof Error && error.message.includes('unknown revision')) {
7006
7032
  try {
7007
- // Get the 'from' commit separately
7008
7033
  const fromCommitLog = await git.log({ from: from, maxCount: 1 });
7009
7034
  const fromCommit = fromCommitLog.latest;
7010
- // Get the range from..to (excluding 'from')
7011
7035
  const rangeLogOptions = {
7012
7036
  from,
7013
7037
  to,
7014
7038
  '--no-merges': noMerges
7015
7039
  };
7016
7040
  const rangeCommitLog = await git.log(rangeLogOptions);
7017
- // Combine the 'from' commit with the range commits
7018
7041
  const allCommits = fromCommit
7019
7042
  ? [fromCommit, ...rangeCommitLog.all]
7020
- : rangeCommitLog.all;
7021
- return allCommits.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
7043
+ : [...rangeCommitLog.all];
7044
+ return allCommits;
7022
7045
  }
7023
7046
  catch (fallbackError) {
7024
7047
  throw fallbackError;
@@ -7045,7 +7068,7 @@ async function getCurrentBranchName({ git }) {
7045
7068
  * @param {SimpleGit} options.git - The SimpleGit instance.
7046
7069
  * @param {Logger} options.logger - The logger for logging messages.
7047
7070
  * @param {string} options.targetBranch - The target branch to compare against.
7048
- * @returns {Promise<string[]>} The array of commit messages in the commit log.
7071
+ * @returns {Promise<CommitDetails[]>} The array of commit messages in the commit log.
7049
7072
  */
7050
7073
  async function getCommitLogAgainstBranch({ git, logger, targetBranch, }) {
7051
7074
  try {
@@ -7064,7 +7087,7 @@ async function getCommitLogAgainstBranch({ git, logger, targetBranch, }) {
7064
7087
  return [];
7065
7088
  }
7066
7089
  // Retrieve commit log with messages
7067
- return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
7090
+ return await getCommitLogRangeDetails(firstCommit, lastCommit, { git, noMerges: true });
7068
7091
  }
7069
7092
  catch (error) {
7070
7093
  logger?.log('Encountered an error getting commit log between branches', { color: 'red' });
@@ -7080,7 +7103,7 @@ async function getCommitLogAgainstBranch({ git, logger, targetBranch, }) {
7080
7103
  * @param {Logger} options.logger - The logger for logging messages.
7081
7104
  * @param {string} [options.comparisonBranch='main'] - The branch to compare against.
7082
7105
  * @param {string} [options.comparisonRemote='origin'] - The remote to compare against.
7083
- * @returns {Promise<string[]>} The array of commit messages in the commit log.
7106
+ * @returns {Promise<CommitDetails[]>} The array of commit messages in the commit log.
7084
7107
  */
7085
7108
  async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main', comparisonRemote = 'origin', }) {
7086
7109
  try {
@@ -7116,7 +7139,7 @@ async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main
7116
7139
  });
7117
7140
  return [];
7118
7141
  }
7119
- return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
7142
+ return await getCommitLogRangeDetails(firstCommit, lastCommit, { git, noMerges: true });
7120
7143
  }
7121
7144
  catch (error) {
7122
7145
  logger?.log('Encountered an error getting commit log from current branch', { color: 'red' });
@@ -7482,18 +7505,202 @@ async function handleResult({ result, mode, interactiveModeCallback }) {
7482
7505
  }
7483
7506
  }
7484
7507
 
7485
- const template$3 = `Write informative git changelog, in the imperative, based on a series of individual messages.
7508
+ /**
7509
+ * Fetches the diff for the given commit ID.
7510
+ *
7511
+ * @param commitId The commit ID for which the diff is to be retrieved.
7512
+ * @returns A promise that resolves to the diff of the commit.
7513
+ */
7514
+ async function getDiffForCommit(commitId, { git, }) {
7515
+ try {
7516
+ return await git.diff(['-p', `${commitId}^..${commitId}`]);
7517
+ }
7518
+ catch (error) {
7519
+ throw new Error(`Error fetching diff for commit ${commitId}: ${error.message}`);
7520
+ }
7521
+ }
7486
7522
 
7487
- - Annotate each change with the git commit hash as reference, including just the first 7 characters
7488
- - Logically group changes, and if necessary, summarize dependency updates
7489
- - Include a descriptive title for the changelog, to give a high-level overview of the changes
7490
- - Depending on the size of the changes, consider breaking the changelog into sections
7491
- - Avoid generlizations like "various bug fixes" or "improvements" or "enhancements"
7523
+ /**
7524
+ * Determines the status of a file based on its changes in the Git repository.
7525
+ *
7526
+ * @param file - The file to check the status of.
7527
+ * @param location - The location to check the status in ('index' or 'working_dir'). Defaults to 'index'.
7528
+ * @returns The status of the file ('added', 'deleted', 'modified', 'renamed', 'untracked', or 'unknown').
7529
+ * @throws Error if the file type is invalid.
7530
+ */
7531
+ function getStatus(file, location = 'index') {
7532
+ if ('index' in file && 'working_dir' in file) {
7533
+ const statusCode = file[location];
7534
+ switch (statusCode) {
7535
+ case 'A':
7536
+ return 'added';
7537
+ case 'D':
7538
+ return 'deleted';
7539
+ case 'M':
7540
+ return 'modified';
7541
+ case 'R':
7542
+ return 'renamed';
7543
+ case '?':
7544
+ return 'untracked';
7545
+ default:
7546
+ return 'unknown';
7547
+ }
7548
+ }
7549
+ else if ('changes' in file && 'binary' in file) {
7550
+ if (file.changes === 0)
7551
+ return 'untracked';
7552
+ if (file.file.includes('=>'))
7553
+ return 'renamed';
7554
+ if (file.deletions === 0 && file.insertions > 0)
7555
+ return 'added';
7556
+ if (file.insertions === 0 && file.deletions > 0)
7557
+ return 'deleted';
7558
+ if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
7559
+ return 'modified';
7560
+ return 'unknown';
7561
+ }
7562
+ else {
7563
+ throw new Error('Invalid file type');
7564
+ }
7565
+ }
7492
7566
 
7567
+ /**
7568
+ * Returns the summary text for a file change.
7569
+ *
7570
+ * @param file - The file status or diff result.
7571
+ * @param change - The partial file change object.
7572
+ * @returns The summary text for the file change.
7573
+ * @throws Error if the file type is invalid.
7574
+ */
7575
+ function getSummaryText(file, change) {
7576
+ const status = change.status || getStatus(file);
7577
+ let filePath;
7578
+ if ('path' in file) {
7579
+ filePath = file.path;
7580
+ }
7581
+ else if ('file' in file) {
7582
+ filePath = change?.filePath || file.file;
7583
+ }
7584
+ else {
7585
+ throw new Error('Invalid file type');
7586
+ }
7587
+ if (change.oldFilePath) {
7588
+ return `${status}: ${change.oldFilePath} -> ${filePath}`;
7589
+ }
7590
+ return `${status}: ${filePath}`;
7591
+ }
7592
+
7593
+ /**
7594
+ * Retrieves the diff between the current branch and a specified target branch.
7595
+ *
7596
+ * @param {Object} options - The options for retrieving the diff.
7597
+ * @param {SimpleGit} options.git - The SimpleGit instance.
7598
+ * @param {Logger} options.logger - The logger for logging messages.
7599
+ * @param {string} options.baseBranch - The base branch to compare against.
7600
+ * @param {string} options.headBranch - The head branch to compare.
7601
+ * @param {string[]} options.ignoredFiles - Array of specific files to ignore.
7602
+ * @param {string[]} options.ignoredExtensions - Array of file extensions to ignore.
7603
+ * @returns {Promise<GetChangesResult>} The diff between the current branch and the target branch.
7604
+ */
7605
+ async function getDiffForBranch({ git, logger, baseBranch, headBranch, options, }) {
7606
+ try {
7607
+ logger?.verbose(`Getting diff for branches: baseBranch="${baseBranch}", headBranch="${headBranch}"`, {
7608
+ color: 'blue',
7609
+ });
7610
+ // Validate branch names
7611
+ if (!baseBranch || !headBranch) {
7612
+ throw new Error(`Invalid branch names: baseBranch="${baseBranch}", headBranch="${headBranch}"`);
7613
+ }
7614
+ const { ignoredFiles = [], ignoredExtensions = [] } = options || {};
7615
+ // Prepare ignore patterns
7616
+ const ignorePatterns = [
7617
+ ...ignoredFiles.map((file) => `:!${file}`),
7618
+ ...ignoredExtensions.map((ext) => `:!*${ext}`),
7619
+ ];
7620
+ // Construct the diff command
7621
+ const diffArgs = [`${baseBranch}..${headBranch}`];
7622
+ if (ignorePatterns.length > 0) {
7623
+ diffArgs.push('--');
7624
+ diffArgs.push(...ignorePatterns);
7625
+ }
7626
+ logger?.verbose(`Running git diff with args: ${diffArgs.join(' ')}`, {
7627
+ color: 'blue',
7628
+ });
7629
+ // Get the diff
7630
+ const diff = await git.diff(diffArgs);
7631
+ logger?.verbose(`Generated diff between "${headBranch}" and "${baseBranch}"`, {
7632
+ color: 'blue',
7633
+ });
7634
+ const changes = diff.split('diff --git').slice(1).map((fileDiff) => {
7635
+ const lines = fileDiff.split('\n');
7636
+ const filePathLine = lines[0];
7637
+ const filePath = filePathLine.split('b/')[1]?.split(' ')[0];
7638
+ const oldFilePath = filePathLine.split('a/')[1]?.split(' ')[0];
7639
+ // Determine status based on diff headers
7640
+ let status = 'modified';
7641
+ if (fileDiff.includes('new file mode')) {
7642
+ status = 'added';
7643
+ }
7644
+ else if (fileDiff.includes('deleted file mode')) {
7645
+ status = 'deleted';
7646
+ }
7647
+ else if (fileDiff.includes('rename from')) {
7648
+ status = 'renamed';
7649
+ }
7650
+ return {
7651
+ filePath: filePath || '',
7652
+ oldFilePath: oldFilePath || '',
7653
+ status,
7654
+ summary: getSummaryText({ path: filePath || '', index: '', working_dir: '' }, { filePath: filePath || '', status }),
7655
+ };
7656
+ });
7657
+ return {
7658
+ staged: changes,
7659
+ unstaged: [],
7660
+ untracked: [],
7661
+ };
7662
+ }
7663
+ catch (error) {
7664
+ const errorMessage = error instanceof Error ? error.message : String(error);
7665
+ console.error('Error in getDiffForBranch:', error);
7666
+ logger?.log(`Encountered an error getting diff between branches: ${errorMessage}`, { color: 'red' });
7667
+ logger?.log(`Branch details: baseBranch="${baseBranch}", headBranch="${headBranch}"`, { color: 'red' });
7668
+ // Re-throw the error so the caller can handle it appropriately
7669
+ throw error;
7670
+ }
7671
+ }
7672
+
7673
+ const template$3 = `You are a highly skilled software engineer tasked with writing a git changelog. Your response should be informative, well-structured, and in the imperative.
7674
+
7675
+ ## Input
7676
+ You will be provided with a summary of changes. This summary can be one of the following:
7677
+ 1. A list of commits, each with its author, hash, message, and body.
7678
+ 2. A list of commits, each with its details AND the full diff of the changes.
7679
+ 3. A single, comprehensive diff for an entire branch.
7680
+
7681
+ ## Rules
7682
+ - Create a descriptive title for the changelog that gives a high-level overview of the changes.
7683
+ - **BREAKING CHANGES**: Identify any commits that introduce breaking changes. These must be listed first under a "### 💥 BREAKING CHANGES" heading.
7684
+ - **Grouping**: Logically group related changes under descriptive headings (e.g., ### Features, ### Fixes, ### Refactors).
7685
+ - **Dependencies**: Group all dependency updates (e.g., changes to package.json, go.mod) under a "### Dependencies" section.
7686
+ - **Summaries**: For each change, provide a concise summary.
7687
+ - **Attribution**: {{author_instructions}}
7688
+ - **Technical Details**: If provided with diffs, use them to understand the technical details and provide a more accurate and detailed description of the changes.
7689
+ - **Clarity**: Avoid generalizations like "various bug fixes," "improvements," or "enhancements." Be specific.
7690
+ - **Formatting**: Your entire response must be valid Markdown.
7691
+
7692
+ ## Formatting Instructions
7493
7693
  {{format_instructions}}
7494
7694
 
7695
+ {{additional_context}}
7696
+
7495
7697
  """{{summary}}"""`;
7496
- const inputVariables$2 = ['format_instructions', 'summary'];
7698
+ const inputVariables$2 = [
7699
+ 'format_instructions',
7700
+ 'summary',
7701
+ 'additional_context',
7702
+ 'author_instructions',
7703
+ ];
7497
7704
  const CHANGELOG_PROMPT = new prompts$1.PromptTemplate({
7498
7705
  template: template$3,
7499
7706
  inputVariables: inputVariables$2,
@@ -7517,48 +7724,68 @@ const handler$4 = async (argv, logger) => {
7517
7724
  }
7518
7725
  async function factory() {
7519
7726
  const branchName = await getCurrentBranchName({ git });
7520
- if (config.sinceLastTag) {
7521
- logger.verbose(`Generating commit log since the last tag`, { color: 'yellow' });
7727
+ if (argv.onlyDiff) {
7728
+ logger.verbose(`Generating changelog based on branch diff`, { color: 'yellow' });
7729
+ const diff = await getDiffForBranch({ git, logger, baseBranch: argv.branch || 'main', headBranch: branchName });
7522
7730
  return {
7523
7731
  branch: branchName,
7524
- commits: await getChangesSinceLastTag({ git, logger }),
7732
+ diff: JSON.stringify(diff.staged, null, 2),
7525
7733
  };
7526
7734
  }
7527
- if (config.range && config.range.includes(':')) {
7735
+ let commits = [];
7736
+ if (config.sinceLastTag) {
7737
+ logger.verbose(`Generating commit log since the last tag`, { color: 'yellow' });
7738
+ // This function returns string[], needs to be adapted or replaced
7739
+ // For now, this path will have limited details.
7740
+ const commitMessages = await getChangesSinceLastTag({ git, logger });
7741
+ commits = commitMessages.map(msg => ({ message: msg }));
7742
+ }
7743
+ else if (config.range && config.range.includes(':')) {
7528
7744
  const [from, to] = config.range.split(':');
7529
7745
  if (!from || !to) {
7530
7746
  logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
7531
7747
  process.exit(1);
7532
7748
  }
7533
- return {
7534
- branch: branchName,
7535
- commits: await getCommitLogRange(from, to, { git, noMerges: true }),
7536
- };
7749
+ commits = await getCommitLogRangeDetails(from, to, { git, noMerges: true });
7537
7750
  }
7538
- if (argv.branch) {
7751
+ else if (argv.branch) {
7539
7752
  logger.verbose(`Generating commit log against branch: ${argv.branch}`, { color: 'yellow' });
7540
- return {
7541
- branch: branchName,
7542
- commits: await getCommitLogAgainstBranch({ git, logger, targetBranch: argv.branch }),
7543
- };
7753
+ commits = await getCommitLogAgainstBranch({ git, logger, targetBranch: argv.branch });
7754
+ }
7755
+ else {
7756
+ logger.verbose(`No range, branch, or tag option provided. Defaulting to current branch`, {
7757
+ color: 'yellow',
7758
+ });
7759
+ commits = await getCommitLogCurrentBranch({ git, logger });
7760
+ }
7761
+ let commitsWithDiffText = commits;
7762
+ if (argv.withDiff) {
7763
+ commitsWithDiffText = await Promise.all(commits.map(async (commit) => ({
7764
+ ...commit,
7765
+ diffText: await getDiffForCommit(commit.hash, { git }),
7766
+ })));
7544
7767
  }
7545
- logger.verbose(`No range, branch, or tag option provided. Defaulting to current branch`, {
7546
- color: 'yellow',
7547
- });
7548
- const commits = await getCommitLogCurrentBranch({ git, logger });
7549
7768
  return {
7550
7769
  branch: branchName,
7551
- commits,
7770
+ commits: commitsWithDiffText,
7771
+ withDiff: argv.withDiff,
7552
7772
  };
7553
7773
  }
7554
- async function parser({ branch, commits }) {
7555
- let result;
7556
- if (!commits || commits.length === 0) {
7557
- result = `## ${branch}\n\nNo commits found.`;
7774
+ async function parser(data) {
7775
+ if (data.diff) {
7776
+ return `## Diff for ${data.branch}\n\n${data.diff}`;
7558
7777
  }
7559
- else {
7560
- result = `## ${branch}\n\n${commits.map((commit) => commit.trim()).join('\n\n')}`;
7778
+ if (!data.commits || data.commits.length === 0) {
7779
+ return `## ${data.branch}\n\nNo commits found.`;
7561
7780
  }
7781
+ let result = `## ${data.branch}\n\n`;
7782
+ result += data.commits.map(commit => {
7783
+ let commitStr = `Author: ${commit.author_name}\nCommit: ${commit.hash}\nMessage: ${commit.message}\n${commit.body}`;
7784
+ if (data.withDiff && commit.diffText) {
7785
+ commitStr += `\nDiff:\n${commit.diffText}`;
7786
+ }
7787
+ return commitStr.trim();
7788
+ }).join('\n\n---\n\n');
7562
7789
  return result;
7563
7790
  }
7564
7791
  const changelogMsg = await generateAndReviewLoop({
@@ -7582,12 +7809,21 @@ const handler$4 = async (argv, logger) => {
7582
7809
  fallback: CHANGELOG_PROMPT,
7583
7810
  });
7584
7811
  const formatInstructions = "Only respond with a valid JSON object, containing two fields: 'title' an escaped string, no more than 65 characters, and 'content' also an escaped string.";
7812
+ let additional_context = '';
7813
+ if (argv.additional) {
7814
+ additional_context = `## Additional Context\n${argv.additional}`;
7815
+ }
7816
+ const author_instructions = argv.author
7817
+ ? 'At the end of each item, attribute the author and include a reference to the commit hash, like this: `by @author_name (f6dbe61)`. Use the first 7 characters of the hash.'
7818
+ : 'At the end of each item, include a reference to the commit hash, like this: `(f6dbe61)`. Use the first 7 characters of the hash.';
7585
7819
  const changelog = await executeChain({
7586
7820
  llm,
7587
7821
  prompt,
7588
7822
  variables: {
7589
7823
  summary: context,
7590
7824
  format_instructions: formatInstructions,
7825
+ additional_context: additional_context,
7826
+ author_instructions: author_instructions,
7591
7827
  },
7592
7828
  parser,
7593
7829
  });
@@ -10899,76 +11135,6 @@ async function createCommit(message, git) {
10899
11135
  return await git.commit(message);
10900
11136
  }
10901
11137
 
10902
- /**
10903
- * Determines the status of a file based on its changes in the Git repository.
10904
- *
10905
- * @param file - The file to check the status of.
10906
- * @param location - The location to check the status in ('index' or 'working_dir'). Defaults to 'index'.
10907
- * @returns The status of the file ('added', 'deleted', 'modified', 'renamed', 'untracked', or 'unknown').
10908
- * @throws Error if the file type is invalid.
10909
- */
10910
- function getStatus(file, location = 'index') {
10911
- if ('index' in file && 'working_dir' in file) {
10912
- const statusCode = file[location];
10913
- switch (statusCode) {
10914
- case 'A':
10915
- return 'added';
10916
- case 'D':
10917
- return 'deleted';
10918
- case 'M':
10919
- return 'modified';
10920
- case 'R':
10921
- return 'renamed';
10922
- case '?':
10923
- return 'untracked';
10924
- default:
10925
- return 'unknown';
10926
- }
10927
- }
10928
- else if ('changes' in file && 'binary' in file) {
10929
- if (file.changes === 0)
10930
- return 'untracked';
10931
- if (file.file.includes('=>'))
10932
- return 'renamed';
10933
- if (file.deletions === 0 && file.insertions > 0)
10934
- return 'added';
10935
- if (file.insertions === 0 && file.deletions > 0)
10936
- return 'deleted';
10937
- if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
10938
- return 'modified';
10939
- return 'unknown';
10940
- }
10941
- else {
10942
- throw new Error('Invalid file type');
10943
- }
10944
- }
10945
-
10946
- /**
10947
- * Returns the summary text for a file change.
10948
- *
10949
- * @param file - The file status or diff result.
10950
- * @param change - The partial file change object.
10951
- * @returns The summary text for the file change.
10952
- * @throws Error if the file type is invalid.
10953
- */
10954
- function getSummaryText(file, change) {
10955
- const status = change.status || getStatus(file);
10956
- let filePath;
10957
- if ('path' in file) {
10958
- filePath = file.path;
10959
- }
10960
- else if ('file' in file) {
10961
- filePath = change?.filePath || file.file;
10962
- }
10963
- else {
10964
- throw new Error('Invalid file type');
10965
- }
10966
- if (change.oldFilePath) {
10967
- return `${status}: ${change.oldFilePath} -> ${filePath}`;
10968
- }
10969
- return `${status}: ${filePath}`;
10970
- }
10971
-
10972
11138
  /**
10973
11139
  * Retrieves the changes in the Git repository.
10974
11140
  *
@@ -12006,86 +12172,6 @@ const getChangesByTimestamp = async ({ since, git }) => {
12006
12172
  return formatCommitLog(commitLog);
12007
12173
  };
12008
12174
 
12009
- /**
12010
- * Retrieves the diff between the current branch and a specified target branch.
12011
- *
12012
- * @param {Object} options - The options for retrieving the diff.
12013
- * @param {SimpleGit} options.git - The SimpleGit instance.
12014
- * @param {Logger} options.logger - The logger for logging messages.
12015
- * @param {string} options.baseBranch - The base branch to compare against.
12016
- * @param {string} options.headBranch - The head branch to compare.
12017
- * @param {string[]} options.ignoredFiles - Array of specific files to ignore.
12018
- * @param {string[]} options.ignoredExtensions - Array of file extensions to ignore.
12019
- * @returns {Promise<GetChangesResult>} The diff between the current branch and the target branch.
12020
- */
12021
- async function getDiffForBranch({ git, logger, baseBranch, headBranch, options, }) {
12022
- try {
12023
- logger?.verbose(`Getting diff for branches: baseBranch="${baseBranch}", headBranch="${headBranch}"`, {
12024
- color: 'blue',
12025
- });
12026
- // Validate branch names
12027
- if (!baseBranch || !headBranch) {
12028
- throw new Error(`Invalid branch names: baseBranch="${baseBranch}", headBranch="${headBranch}"`);
12029
- }
12030
- const { ignoredFiles = [], ignoredExtensions = [] } = options || {};
12031
- // Prepare ignore patterns
12032
- const ignorePatterns = [
12033
- ...ignoredFiles.map((file) => `:!${file}`),
12034
- ...ignoredExtensions.map((ext) => `:!*${ext}`),
12035
- ];
12036
- // Construct the diff command
12037
- const diffArgs = [`${baseBranch}..${headBranch}`];
12038
- if (ignorePatterns.length > 0) {
12039
- diffArgs.push('--');
12040
- diffArgs.push(...ignorePatterns);
12041
- }
12042
- logger?.verbose(`Running git diff with args: ${diffArgs.join(' ')}`, {
12043
- color: 'blue',
12044
- });
12045
- // Get the diff
12046
- const diff = await git.diff(diffArgs);
12047
- logger?.verbose(`Generated diff between "${headBranch}" and "${baseBranch}"`, {
12048
- color: 'blue',
12049
- });
12050
- const changes = diff.split('diff --git').slice(1).map((fileDiff) => {
12051
- const lines = fileDiff.split('\n');
12052
- const filePathLine = lines[0];
12053
- const filePath = filePathLine.split('b/')[1]?.split(' ')[0];
12054
- const oldFilePath = filePathLine.split('a/')[1]?.split(' ')[0];
12055
- // Determine status based on diff headers
12056
- let status = 'modified';
12057
- if (fileDiff.includes('new file mode')) {
12058
- status = 'added';
12059
- }
12060
- else if (fileDiff.includes('deleted file mode')) {
12061
- status = 'deleted';
12062
- }
12063
- else if (fileDiff.includes('rename from')) {
12064
- status = 'renamed';
12065
- }
12066
- return {
12067
- filePath: filePath || '',
12068
- oldFilePath: oldFilePath || '',
12069
- status,
12070
- summary: getSummaryText({ path: filePath || '', index: '', working_dir: '' }, { filePath: filePath || '', status }),
12071
- };
12072
- });
12073
- return {
12074
- staged: changes,
12075
- unstaged: [],
12076
- untracked: [],
12077
- };
12078
- }
12079
- catch (error) {
12080
- const errorMessage = error instanceof Error ? error.message : String(error);
12081
- console.error('Error in getDiffForBranch:', error);
12082
- logger?.log(`Encountered an error getting diff between branches: ${errorMessage}`, { color: 'red' });
12083
- logger?.log(`Branch details: baseBranch="${baseBranch}", headBranch="${headBranch}"`, { color: 'red' });
12084
- // Re-throw the error so the caller can handle it appropriately
12085
- throw error;
12086
- }
12087
- }
12088
-
12089
12175
  async function noResult$1({ logger }) {
12090
12176
  logger.log('No repo changes detected. 👀', { color: 'blue' });
12091
12177
  throw new Error('NO_CHANGES_DETECTED');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-coco",
3
- "version": "0.21.4",
3
+ "version": "0.22.0",
4
4
  "description": "zero-effort git commits with coco.",
5
5
  "author": "gfargo <ghfargo@gmail.com>",
6
6
  "license": "MIT",