git-coco 0.21.4 → 0.22.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/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.1";
52
52
 
53
53
  const isInteractive = (config) => {
54
54
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -1018,6 +1018,10 @@ 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
+ "$ref": "#/definitions/OpenAIVerbosityParam",
1024
+ "description": "The verbosity of the model's response."
1021
1025
  }
1022
1026
  }
1023
1027
  },
@@ -1590,6 +1594,18 @@ const schema$1 = {
1590
1594
  "gpt-3.5-turbo-16k-0613"
1591
1595
  ]
1592
1596
  },
1597
+ "OpenAIVerbosityParam": {
1598
+ "type": [
1599
+ "string",
1600
+ "null"
1601
+ ],
1602
+ "enum": [
1603
+ "low",
1604
+ "medium",
1605
+ "high",
1606
+ null
1607
+ ]
1608
+ },
1593
1609
  "OllamaLLMService": {
1594
1610
  "type": "object",
1595
1611
  "additionalProperties": false,
@@ -6054,6 +6070,26 @@ const options$4 = {
6054
6070
  description: 'Generate changelog for all commits since the last tag',
6055
6071
  default: false,
6056
6072
  },
6073
+ withDiff: {
6074
+ type: 'boolean',
6075
+ description: 'Include the diff for each commit in the prompt',
6076
+ default: false,
6077
+ },
6078
+ onlyDiff: {
6079
+ type: 'boolean',
6080
+ description: 'Generate a changelog based only on the diff of the entire branch',
6081
+ default: false,
6082
+ },
6083
+ additional: {
6084
+ type: 'string',
6085
+ alias: 'a',
6086
+ description: 'Add extra contextual information to the prompt',
6087
+ },
6088
+ author: {
6089
+ type: 'boolean',
6090
+ description: 'Include author attribution in the changelog',
6091
+ default: false,
6092
+ },
6057
6093
  i: {
6058
6094
  type: 'boolean',
6059
6095
  alias: 'interactive',
@@ -6958,45 +6994,39 @@ const getChangesSinceLastTag = async ({ git }) => {
6958
6994
  };
6959
6995
 
6960
6996
  /**
6961
- * Retrieves the commit log range between two specified commits (inclusive of both commits).
6997
+ * Retrieves the detailed commit log range between two specified commits (inclusive of both commits).
6962
6998
  *
6963
6999
  * @param from - The starting commit (can be a commit hash, HEAD reference, or branch name). This commit will be included in the results.
6964
7000
  * @param to - The ending commit (can be a commit hash, HEAD reference, or branch name). This commit will be included in the results.
6965
7001
  * @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.
7002
+ * @returns A promise that resolves to an array of commit details objects.
6967
7003
  * @throws If there is an error retrieving the commit log range.
6968
7004
  */
6969
- async function getCommitLogRange(from, to, { noMerges, git }) {
7005
+ async function getCommitLogRangeDetails(from, to, { noMerges, git }) {
6970
7006
  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
7007
  const logOptions = {
6974
7008
  from: `${from}^`,
6975
7009
  to,
6976
7010
  '--no-merges': noMerges
6977
7011
  };
6978
7012
  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}>`);
7013
+ return [...commitLog.all];
6980
7014
  }
6981
7015
  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
7016
  if (error instanceof Error && error.message.includes('unknown revision')) {
6984
7017
  try {
6985
- // Get the 'from' commit separately
6986
7018
  const fromCommitLog = await git.log({ from: from, maxCount: 1 });
6987
7019
  const fromCommit = fromCommitLog.latest;
6988
- // Get the range from..to (excluding 'from')
6989
7020
  const rangeLogOptions = {
6990
7021
  from,
6991
7022
  to,
6992
7023
  '--no-merges': noMerges
6993
7024
  };
6994
7025
  const rangeCommitLog = await git.log(rangeLogOptions);
6995
- // Combine the 'from' commit with the range commits
6996
7026
  const allCommits = fromCommit
6997
7027
  ? [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}>`);
7028
+ : [...rangeCommitLog.all];
7029
+ return allCommits;
7000
7030
  }
7001
7031
  catch (fallbackError) {
7002
7032
  throw fallbackError;
@@ -7023,7 +7053,7 @@ async function getCurrentBranchName({ git }) {
7023
7053
  * @param {SimpleGit} options.git - The SimpleGit instance.
7024
7054
  * @param {Logger} options.logger - The logger for logging messages.
7025
7055
  * @param {string} options.targetBranch - The target branch to compare against.
7026
- * @returns {Promise<string[]>} The array of commit messages in the commit log.
7056
+ * @returns {Promise<CommitDetails[]>} The array of commit messages in the commit log.
7027
7057
  */
7028
7058
  async function getCommitLogAgainstBranch({ git, logger, targetBranch, }) {
7029
7059
  try {
@@ -7042,7 +7072,7 @@ async function getCommitLogAgainstBranch({ git, logger, targetBranch, }) {
7042
7072
  return [];
7043
7073
  }
7044
7074
  // Retrieve commit log with messages
7045
- return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
7075
+ return await getCommitLogRangeDetails(firstCommit, lastCommit, { git, noMerges: true });
7046
7076
  }
7047
7077
  catch (error) {
7048
7078
  logger?.log('Encountered an error getting commit log between branches', { color: 'red' });
@@ -7058,7 +7088,7 @@ async function getCommitLogAgainstBranch({ git, logger, targetBranch, }) {
7058
7088
  * @param {Logger} options.logger - The logger for logging messages.
7059
7089
  * @param {string} [options.comparisonBranch='main'] - The branch to compare against.
7060
7090
  * @param {string} [options.comparisonRemote='origin'] - The remote to compare against.
7061
- * @returns {Promise<string[]>} The array of commit messages in the commit log.
7091
+ * @returns {Promise<CommitDetails[]>} The array of commit messages in the commit log.
7062
7092
  */
7063
7093
  async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main', comparisonRemote = 'origin', }) {
7064
7094
  try {
@@ -7094,7 +7124,7 @@ async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main
7094
7124
  });
7095
7125
  return [];
7096
7126
  }
7097
- return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
7127
+ return await getCommitLogRangeDetails(firstCommit, lastCommit, { git, noMerges: true });
7098
7128
  }
7099
7129
  catch (error) {
7100
7130
  logger?.log('Encountered an error getting commit log from current branch', { color: 'red' });
@@ -7460,18 +7490,202 @@ async function handleResult({ result, mode, interactiveModeCallback }) {
7460
7490
  }
7461
7491
  }
7462
7492
 
7463
- const template$3 = `Write informative git changelog, in the imperative, based on a series of individual messages.
7493
+ /**
7494
+ * Fetches the diff for the given commit ID.
7495
+ *
7496
+ * @param commitId The commit ID for which the diff is to be retrieved.
7497
+ * @returns A promise that resolves to the diff of the commit.
7498
+ */
7499
+ async function getDiffForCommit(commitId, { git, }) {
7500
+ try {
7501
+ return await git.diff(['-p', `${commitId}^..${commitId}`]);
7502
+ }
7503
+ catch (error) {
7504
+ throw new Error(`Error fetching diff for commit ${commitId}: ${error.message}`);
7505
+ }
7506
+ }
7507
+
7508
+ /**
7509
+ * Determines the status of a file based on its changes in the Git repository.
7510
+ *
7511
+ * @param file - The file to check the status of.
7512
+ * @param location - The location to check the status in ('index' or 'working_dir'). Defaults to 'index'.
7513
+ * @returns The status of the file ('added', 'deleted', 'modified', 'renamed', 'untracked', or 'unknown').
7514
+ * @throws Error if the file type is invalid.
7515
+ */
7516
+ function getStatus(file, location = 'index') {
7517
+ if ('index' in file && 'working_dir' in file) {
7518
+ const statusCode = file[location];
7519
+ switch (statusCode) {
7520
+ case 'A':
7521
+ return 'added';
7522
+ case 'D':
7523
+ return 'deleted';
7524
+ case 'M':
7525
+ return 'modified';
7526
+ case 'R':
7527
+ return 'renamed';
7528
+ case '?':
7529
+ return 'untracked';
7530
+ default:
7531
+ return 'unknown';
7532
+ }
7533
+ }
7534
+ else if ('changes' in file && 'binary' in file) {
7535
+ if (file.changes === 0)
7536
+ return 'untracked';
7537
+ if (file.file.includes('=>'))
7538
+ return 'renamed';
7539
+ if (file.deletions === 0 && file.insertions > 0)
7540
+ return 'added';
7541
+ if (file.insertions === 0 && file.deletions > 0)
7542
+ return 'deleted';
7543
+ if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
7544
+ return 'modified';
7545
+ return 'unknown';
7546
+ }
7547
+ else {
7548
+ throw new Error('Invalid file type');
7549
+ }
7550
+ }
7551
+
7552
+ /**
7553
+ * Returns the summary text for a file change.
7554
+ *
7555
+ * @param file - The file status or diff result.
7556
+ * @param change - The partial file change object.
7557
+ * @returns The summary text for the file change.
7558
+ * @throws Error if the file type is invalid.
7559
+ */
7560
+ function getSummaryText(file, change) {
7561
+ const status = change.status || getStatus(file);
7562
+ let filePath;
7563
+ if ('path' in file) {
7564
+ filePath = file.path;
7565
+ }
7566
+ else if ('file' in file) {
7567
+ filePath = change?.filePath || file.file;
7568
+ }
7569
+ else {
7570
+ throw new Error('Invalid file type');
7571
+ }
7572
+ if (change.oldFilePath) {
7573
+ return `${status}: ${change.oldFilePath} -> ${filePath}`;
7574
+ }
7575
+ return `${status}: ${filePath}`;
7576
+ }
7577
+
7578
+ /**
7579
+ * Retrieves the diff between the current branch and a specified target branch.
7580
+ *
7581
+ * @param {Object} options - The options for retrieving the diff.
7582
+ * @param {SimpleGit} options.git - The SimpleGit instance.
7583
+ * @param {Logger} options.logger - The logger for logging messages.
7584
+ * @param {string} options.baseBranch - The base branch to compare against.
7585
+ * @param {string} options.headBranch - The head branch to compare.
7586
+ * @param {string[]} options.ignoredFiles - Array of specific files to ignore.
7587
+ * @param {string[]} options.ignoredExtensions - Array of file extensions to ignore.
7588
+ * @returns {Promise<GetChangesResult>} The diff between the current branch and the target branch.
7589
+ */
7590
+ async function getDiffForBranch({ git, logger, baseBranch, headBranch, options, }) {
7591
+ try {
7592
+ logger?.verbose(`Getting diff for branches: baseBranch="${baseBranch}", headBranch="${headBranch}"`, {
7593
+ color: 'blue',
7594
+ });
7595
+ // Validate branch names
7596
+ if (!baseBranch || !headBranch) {
7597
+ throw new Error(`Invalid branch names: baseBranch="${baseBranch}", headBranch="${headBranch}"`);
7598
+ }
7599
+ const { ignoredFiles = [], ignoredExtensions = [] } = options || {};
7600
+ // Prepare ignore patterns
7601
+ const ignorePatterns = [
7602
+ ...ignoredFiles.map((file) => `:!${file}`),
7603
+ ...ignoredExtensions.map((ext) => `:!*${ext}`),
7604
+ ];
7605
+ // Construct the diff command
7606
+ const diffArgs = [`${baseBranch}..${headBranch}`];
7607
+ if (ignorePatterns.length > 0) {
7608
+ diffArgs.push('--');
7609
+ diffArgs.push(...ignorePatterns);
7610
+ }
7611
+ logger?.verbose(`Running git diff with args: ${diffArgs.join(' ')}`, {
7612
+ color: 'blue',
7613
+ });
7614
+ // Get the diff
7615
+ const diff = await git.diff(diffArgs);
7616
+ logger?.verbose(`Generated diff between "${headBranch}" and "${baseBranch}"`, {
7617
+ color: 'blue',
7618
+ });
7619
+ const changes = diff.split('diff --git').slice(1).map((fileDiff) => {
7620
+ const lines = fileDiff.split('\n');
7621
+ const filePathLine = lines[0];
7622
+ const filePath = filePathLine.split('b/')[1]?.split(' ')[0];
7623
+ const oldFilePath = filePathLine.split('a/')[1]?.split(' ')[0];
7624
+ // Determine status based on diff headers
7625
+ let status = 'modified';
7626
+ if (fileDiff.includes('new file mode')) {
7627
+ status = 'added';
7628
+ }
7629
+ else if (fileDiff.includes('deleted file mode')) {
7630
+ status = 'deleted';
7631
+ }
7632
+ else if (fileDiff.includes('rename from')) {
7633
+ status = 'renamed';
7634
+ }
7635
+ return {
7636
+ filePath: filePath || '',
7637
+ oldFilePath: oldFilePath || '',
7638
+ status,
7639
+ summary: getSummaryText({ path: filePath || '', index: '', working_dir: '' }, { filePath: filePath || '', status }),
7640
+ };
7641
+ });
7642
+ return {
7643
+ staged: changes,
7644
+ unstaged: [],
7645
+ untracked: [],
7646
+ };
7647
+ }
7648
+ catch (error) {
7649
+ const errorMessage = error instanceof Error ? error.message : String(error);
7650
+ console.error('Error in getDiffForBranch:', error);
7651
+ logger?.log(`Encountered an error getting diff between branches: ${errorMessage}`, { color: 'red' });
7652
+ logger?.log(`Branch details: baseBranch="${baseBranch}", headBranch="${headBranch}"`, { color: 'red' });
7653
+ // Re-throw the error so the caller can handle it appropriately
7654
+ throw error;
7655
+ }
7656
+ }
7657
+
7658
+ 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.
7464
7659
 
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"
7660
+ ## Input
7661
+ You will be provided with a summary of changes. This summary can be one of the following:
7662
+ 1. A list of commits, each with its author, hash, message, and body.
7663
+ 2. A list of commits, each with its details AND the full diff of the changes.
7664
+ 3. A single, comprehensive diff for an entire branch.
7470
7665
 
7666
+ ## Rules
7667
+ - Create a descriptive title for the changelog that gives a high-level overview of the changes.
7668
+ - **BREAKING CHANGES**: Identify any commits that introduce breaking changes. These must be listed first under a "### 💥 BREAKING CHANGES" heading.
7669
+ - **Grouping**: Logically group related changes under descriptive headings (e.g., ### Features, ### Fixes, ### Refactors).
7670
+ - **Dependencies**: Group all dependency updates (e.g., changes to package.json, go.mod) under a "### Dependencies" section.
7671
+ - **Summaries**: For each change, provide a concise summary.
7672
+ - **Attribution**: {{author_instructions}}
7673
+ - **Technical Details**: If provided with diffs, use them to understand the technical details and provide a more accurate and detailed description of the changes.
7674
+ - **Clarity**: Avoid generalizations like "various bug fixes," "improvements," or "enhancements." Be specific.
7675
+ - **Formatting**: Your entire response must be valid Markdown.
7676
+
7677
+ ## Formatting Instructions
7471
7678
  {{format_instructions}}
7472
7679
 
7680
+ {{additional_context}}
7681
+
7473
7682
  """{{summary}}"""`;
7474
- const inputVariables$2 = ['format_instructions', 'summary'];
7683
+ const inputVariables$2 = [
7684
+ 'format_instructions',
7685
+ 'summary',
7686
+ 'additional_context',
7687
+ 'author_instructions',
7688
+ ];
7475
7689
  const CHANGELOG_PROMPT = new PromptTemplate({
7476
7690
  template: template$3,
7477
7691
  inputVariables: inputVariables$2,
@@ -7495,48 +7709,68 @@ const handler$4 = async (argv, logger) => {
7495
7709
  }
7496
7710
  async function factory() {
7497
7711
  const branchName = await getCurrentBranchName({ git });
7498
- if (config.sinceLastTag) {
7499
- logger.verbose(`Generating commit log since the last tag`, { color: 'yellow' });
7712
+ if (argv.onlyDiff) {
7713
+ logger.verbose(`Generating changelog based on branch diff`, { color: 'yellow' });
7714
+ const diff = await getDiffForBranch({ git, logger, baseBranch: argv.branch || 'main', headBranch: branchName });
7500
7715
  return {
7501
7716
  branch: branchName,
7502
- commits: await getChangesSinceLastTag({ git, logger }),
7717
+ diff: JSON.stringify(diff.staged, null, 2),
7503
7718
  };
7504
7719
  }
7505
- if (config.range && config.range.includes(':')) {
7720
+ let commits = [];
7721
+ if (config.sinceLastTag) {
7722
+ logger.verbose(`Generating commit log since the last tag`, { color: 'yellow' });
7723
+ // This function returns string[], needs to be adapted or replaced
7724
+ // For now, this path will have limited details.
7725
+ const commitMessages = await getChangesSinceLastTag({ git, logger });
7726
+ commits = commitMessages.map(msg => ({ message: msg }));
7727
+ }
7728
+ else if (config.range && config.range.includes(':')) {
7506
7729
  const [from, to] = config.range.split(':');
7507
7730
  if (!from || !to) {
7508
7731
  logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
7509
7732
  process.exit(1);
7510
7733
  }
7511
- return {
7512
- branch: branchName,
7513
- commits: await getCommitLogRange(from, to, { git, noMerges: true }),
7514
- };
7734
+ commits = await getCommitLogRangeDetails(from, to, { git, noMerges: true });
7515
7735
  }
7516
- if (argv.branch) {
7736
+ else if (argv.branch) {
7517
7737
  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
- };
7738
+ commits = await getCommitLogAgainstBranch({ git, logger, targetBranch: argv.branch });
7739
+ }
7740
+ else {
7741
+ logger.verbose(`No range, branch, or tag option provided. Defaulting to current branch`, {
7742
+ color: 'yellow',
7743
+ });
7744
+ commits = await getCommitLogCurrentBranch({ git, logger });
7745
+ }
7746
+ let commitsWithDiffText = commits;
7747
+ if (argv.withDiff) {
7748
+ commitsWithDiffText = await Promise.all(commits.map(async (commit) => ({
7749
+ ...commit,
7750
+ diffText: await getDiffForCommit(commit.hash, { git }),
7751
+ })));
7522
7752
  }
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
7753
  return {
7528
7754
  branch: branchName,
7529
- commits,
7755
+ commits: commitsWithDiffText,
7756
+ withDiff: argv.withDiff,
7530
7757
  };
7531
7758
  }
7532
- async function parser({ branch, commits }) {
7533
- let result;
7534
- if (!commits || commits.length === 0) {
7535
- result = `## ${branch}\n\nNo commits found.`;
7759
+ async function parser(data) {
7760
+ if (data.diff) {
7761
+ return `## Diff for ${data.branch}\n\n${data.diff}`;
7536
7762
  }
7537
- else {
7538
- result = `## ${branch}\n\n${commits.map((commit) => commit.trim()).join('\n\n')}`;
7763
+ if (!data.commits || data.commits.length === 0) {
7764
+ return `## ${data.branch}\n\nNo commits found.`;
7539
7765
  }
7766
+ let result = `## ${data.branch}\n\n`;
7767
+ result += data.commits.map(commit => {
7768
+ let commitStr = `Author: ${commit.author_name}\nCommit: ${commit.hash}\nMessage: ${commit.message}\n${commit.body}`;
7769
+ if (data.withDiff && commit.diffText) {
7770
+ commitStr += `\nDiff:\n${commit.diffText}`;
7771
+ }
7772
+ return commitStr.trim();
7773
+ }).join('\n\n---\n\n');
7540
7774
  return result;
7541
7775
  }
7542
7776
  const changelogMsg = await generateAndReviewLoop({
@@ -7560,12 +7794,21 @@ const handler$4 = async (argv, logger) => {
7560
7794
  fallback: CHANGELOG_PROMPT,
7561
7795
  });
7562
7796
  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.";
7797
+ let additional_context = '';
7798
+ if (argv.additional) {
7799
+ additional_context = `## Additional Context\n${argv.additional}`;
7800
+ }
7801
+ const author_instructions = argv.author
7802
+ ? '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.'
7803
+ : '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
7804
  const changelog = await executeChain({
7564
7805
  llm,
7565
7806
  prompt,
7566
7807
  variables: {
7567
7808
  summary: context,
7568
7809
  format_instructions: formatInstructions,
7810
+ additional_context: additional_context,
7811
+ author_instructions: author_instructions,
7569
7812
  },
7570
7813
  parser,
7571
7814
  });
@@ -10877,76 +11120,6 @@ async function createCommit(message, git) {
10877
11120
  return await git.commit(message);
10878
11121
  }
10879
11122
 
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
11123
  /**
10951
11124
  * Retrieves the changes in the Git repository.
10952
11125
  *
@@ -11264,7 +11437,7 @@ const handler$3 = async (argv, logger) => {
11264
11437
  const schema = USE_CONVENTIONAL_COMMITS
11265
11438
  ? ConventionalCommitMessageResponseSchema
11266
11439
  : CommitMessageResponseSchema;
11267
- const formatInstructions = `You must always return valid JSON fenced by a markdown code block. Do not return any additional text. The JSON object you return should match the following schema:
11440
+ const formatInstructions = `You must always return a valid JSON object. Do not return any additional text. The JSON object you return should match the following schema:
11268
11441
  ${schema.description}
11269
11442
  {
11270
11443
  "title": "The commit title",
@@ -11984,86 +12157,6 @@ const getChangesByTimestamp = async ({ since, git }) => {
11984
12157
  return formatCommitLog(commitLog);
11985
12158
  };
11986
12159
 
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
12160
  async function noResult$1({ logger }) {
12068
12161
  logger.log('No repo changes detected. 👀', { color: 'blue' });
12069
12162
  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.1";
74
74
 
75
75
  const isInteractive = (config) => {
76
76
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -1040,6 +1040,10 @@ 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
+ "$ref": "#/definitions/OpenAIVerbosityParam",
1046
+ "description": "The verbosity of the model's response."
1043
1047
  }
1044
1048
  }
1045
1049
  },
@@ -1612,6 +1616,18 @@ const schema$1 = {
1612
1616
  "gpt-3.5-turbo-16k-0613"
1613
1617
  ]
1614
1618
  },
1619
+ "OpenAIVerbosityParam": {
1620
+ "type": [
1621
+ "string",
1622
+ "null"
1623
+ ],
1624
+ "enum": [
1625
+ "low",
1626
+ "medium",
1627
+ "high",
1628
+ null
1629
+ ]
1630
+ },
1615
1631
  "OllamaLLMService": {
1616
1632
  "type": "object",
1617
1633
  "additionalProperties": false,
@@ -6076,6 +6092,26 @@ const options$4 = {
6076
6092
  description: 'Generate changelog for all commits since the last tag',
6077
6093
  default: false,
6078
6094
  },
6095
+ withDiff: {
6096
+ type: 'boolean',
6097
+ description: 'Include the diff for each commit in the prompt',
6098
+ default: false,
6099
+ },
6100
+ onlyDiff: {
6101
+ type: 'boolean',
6102
+ description: 'Generate a changelog based only on the diff of the entire branch',
6103
+ default: false,
6104
+ },
6105
+ additional: {
6106
+ type: 'string',
6107
+ alias: 'a',
6108
+ description: 'Add extra contextual information to the prompt',
6109
+ },
6110
+ author: {
6111
+ type: 'boolean',
6112
+ description: 'Include author attribution in the changelog',
6113
+ default: false,
6114
+ },
6079
6115
  i: {
6080
6116
  type: 'boolean',
6081
6117
  alias: 'interactive',
@@ -6980,45 +7016,39 @@ const getChangesSinceLastTag = async ({ git }) => {
6980
7016
  };
6981
7017
 
6982
7018
  /**
6983
- * Retrieves the commit log range between two specified commits (inclusive of both commits).
7019
+ * Retrieves the detailed commit log range between two specified commits (inclusive of both commits).
6984
7020
  *
6985
7021
  * @param from - The starting commit (can be a commit hash, HEAD reference, or branch name). This commit will be included in the results.
6986
7022
  * @param to - The ending commit (can be a commit hash, HEAD reference, or branch name). This commit will be included in the results.
6987
7023
  * @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.
7024
+ * @returns A promise that resolves to an array of commit details objects.
6989
7025
  * @throws If there is an error retrieving the commit log range.
6990
7026
  */
6991
- async function getCommitLogRange(from, to, { noMerges, git }) {
7027
+ async function getCommitLogRangeDetails(from, to, { noMerges, git }) {
6992
7028
  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
7029
  const logOptions = {
6996
7030
  from: `${from}^`,
6997
7031
  to,
6998
7032
  '--no-merges': noMerges
6999
7033
  };
7000
7034
  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}>`);
7035
+ return [...commitLog.all];
7002
7036
  }
7003
7037
  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
7038
  if (error instanceof Error && error.message.includes('unknown revision')) {
7006
7039
  try {
7007
- // Get the 'from' commit separately
7008
7040
  const fromCommitLog = await git.log({ from: from, maxCount: 1 });
7009
7041
  const fromCommit = fromCommitLog.latest;
7010
- // Get the range from..to (excluding 'from')
7011
7042
  const rangeLogOptions = {
7012
7043
  from,
7013
7044
  to,
7014
7045
  '--no-merges': noMerges
7015
7046
  };
7016
7047
  const rangeCommitLog = await git.log(rangeLogOptions);
7017
- // Combine the 'from' commit with the range commits
7018
7048
  const allCommits = fromCommit
7019
7049
  ? [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}>`);
7050
+ : [...rangeCommitLog.all];
7051
+ return allCommits;
7022
7052
  }
7023
7053
  catch (fallbackError) {
7024
7054
  throw fallbackError;
@@ -7045,7 +7075,7 @@ async function getCurrentBranchName({ git }) {
7045
7075
  * @param {SimpleGit} options.git - The SimpleGit instance.
7046
7076
  * @param {Logger} options.logger - The logger for logging messages.
7047
7077
  * @param {string} options.targetBranch - The target branch to compare against.
7048
- * @returns {Promise<string[]>} The array of commit messages in the commit log.
7078
+ * @returns {Promise<CommitDetails[]>} The array of commit messages in the commit log.
7049
7079
  */
7050
7080
  async function getCommitLogAgainstBranch({ git, logger, targetBranch, }) {
7051
7081
  try {
@@ -7064,7 +7094,7 @@ async function getCommitLogAgainstBranch({ git, logger, targetBranch, }) {
7064
7094
  return [];
7065
7095
  }
7066
7096
  // Retrieve commit log with messages
7067
- return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
7097
+ return await getCommitLogRangeDetails(firstCommit, lastCommit, { git, noMerges: true });
7068
7098
  }
7069
7099
  catch (error) {
7070
7100
  logger?.log('Encountered an error getting commit log between branches', { color: 'red' });
@@ -7080,7 +7110,7 @@ async function getCommitLogAgainstBranch({ git, logger, targetBranch, }) {
7080
7110
  * @param {Logger} options.logger - The logger for logging messages.
7081
7111
  * @param {string} [options.comparisonBranch='main'] - The branch to compare against.
7082
7112
  * @param {string} [options.comparisonRemote='origin'] - The remote to compare against.
7083
- * @returns {Promise<string[]>} The array of commit messages in the commit log.
7113
+ * @returns {Promise<CommitDetails[]>} The array of commit messages in the commit log.
7084
7114
  */
7085
7115
  async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main', comparisonRemote = 'origin', }) {
7086
7116
  try {
@@ -7116,7 +7146,7 @@ async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main
7116
7146
  });
7117
7147
  return [];
7118
7148
  }
7119
- return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
7149
+ return await getCommitLogRangeDetails(firstCommit, lastCommit, { git, noMerges: true });
7120
7150
  }
7121
7151
  catch (error) {
7122
7152
  logger?.log('Encountered an error getting commit log from current branch', { color: 'red' });
@@ -7482,18 +7512,202 @@ async function handleResult({ result, mode, interactiveModeCallback }) {
7482
7512
  }
7483
7513
  }
7484
7514
 
7485
- const template$3 = `Write informative git changelog, in the imperative, based on a series of individual messages.
7515
+ /**
7516
+ * Fetches the diff for the given commit ID.
7517
+ *
7518
+ * @param commitId The commit ID for which the diff is to be retrieved.
7519
+ * @returns A promise that resolves to the diff of the commit.
7520
+ */
7521
+ async function getDiffForCommit(commitId, { git, }) {
7522
+ try {
7523
+ return await git.diff(['-p', `${commitId}^..${commitId}`]);
7524
+ }
7525
+ catch (error) {
7526
+ throw new Error(`Error fetching diff for commit ${commitId}: ${error.message}`);
7527
+ }
7528
+ }
7529
+
7530
+ /**
7531
+ * Determines the status of a file based on its changes in the Git repository.
7532
+ *
7533
+ * @param file - The file to check the status of.
7534
+ * @param location - The location to check the status in ('index' or 'working_dir'). Defaults to 'index'.
7535
+ * @returns The status of the file ('added', 'deleted', 'modified', 'renamed', 'untracked', or 'unknown').
7536
+ * @throws Error if the file type is invalid.
7537
+ */
7538
+ function getStatus(file, location = 'index') {
7539
+ if ('index' in file && 'working_dir' in file) {
7540
+ const statusCode = file[location];
7541
+ switch (statusCode) {
7542
+ case 'A':
7543
+ return 'added';
7544
+ case 'D':
7545
+ return 'deleted';
7546
+ case 'M':
7547
+ return 'modified';
7548
+ case 'R':
7549
+ return 'renamed';
7550
+ case '?':
7551
+ return 'untracked';
7552
+ default:
7553
+ return 'unknown';
7554
+ }
7555
+ }
7556
+ else if ('changes' in file && 'binary' in file) {
7557
+ if (file.changes === 0)
7558
+ return 'untracked';
7559
+ if (file.file.includes('=>'))
7560
+ return 'renamed';
7561
+ if (file.deletions === 0 && file.insertions > 0)
7562
+ return 'added';
7563
+ if (file.insertions === 0 && file.deletions > 0)
7564
+ return 'deleted';
7565
+ if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
7566
+ return 'modified';
7567
+ return 'unknown';
7568
+ }
7569
+ else {
7570
+ throw new Error('Invalid file type');
7571
+ }
7572
+ }
7573
+
7574
+ /**
7575
+ * Returns the summary text for a file change.
7576
+ *
7577
+ * @param file - The file status or diff result.
7578
+ * @param change - The partial file change object.
7579
+ * @returns The summary text for the file change.
7580
+ * @throws Error if the file type is invalid.
7581
+ */
7582
+ function getSummaryText(file, change) {
7583
+ const status = change.status || getStatus(file);
7584
+ let filePath;
7585
+ if ('path' in file) {
7586
+ filePath = file.path;
7587
+ }
7588
+ else if ('file' in file) {
7589
+ filePath = change?.filePath || file.file;
7590
+ }
7591
+ else {
7592
+ throw new Error('Invalid file type');
7593
+ }
7594
+ if (change.oldFilePath) {
7595
+ return `${status}: ${change.oldFilePath} -> ${filePath}`;
7596
+ }
7597
+ return `${status}: ${filePath}`;
7598
+ }
7599
+
7600
+ /**
7601
+ * Retrieves the diff between the current branch and a specified target branch.
7602
+ *
7603
+ * @param {Object} options - The options for retrieving the diff.
7604
+ * @param {SimpleGit} options.git - The SimpleGit instance.
7605
+ * @param {Logger} options.logger - The logger for logging messages.
7606
+ * @param {string} options.baseBranch - The base branch to compare against.
7607
+ * @param {string} options.headBranch - The head branch to compare.
7608
+ * @param {string[]} options.ignoredFiles - Array of specific files to ignore.
7609
+ * @param {string[]} options.ignoredExtensions - Array of file extensions to ignore.
7610
+ * @returns {Promise<GetChangesResult>} The diff between the current branch and the target branch.
7611
+ */
7612
+ async function getDiffForBranch({ git, logger, baseBranch, headBranch, options, }) {
7613
+ try {
7614
+ logger?.verbose(`Getting diff for branches: baseBranch="${baseBranch}", headBranch="${headBranch}"`, {
7615
+ color: 'blue',
7616
+ });
7617
+ // Validate branch names
7618
+ if (!baseBranch || !headBranch) {
7619
+ throw new Error(`Invalid branch names: baseBranch="${baseBranch}", headBranch="${headBranch}"`);
7620
+ }
7621
+ const { ignoredFiles = [], ignoredExtensions = [] } = options || {};
7622
+ // Prepare ignore patterns
7623
+ const ignorePatterns = [
7624
+ ...ignoredFiles.map((file) => `:!${file}`),
7625
+ ...ignoredExtensions.map((ext) => `:!*${ext}`),
7626
+ ];
7627
+ // Construct the diff command
7628
+ const diffArgs = [`${baseBranch}..${headBranch}`];
7629
+ if (ignorePatterns.length > 0) {
7630
+ diffArgs.push('--');
7631
+ diffArgs.push(...ignorePatterns);
7632
+ }
7633
+ logger?.verbose(`Running git diff with args: ${diffArgs.join(' ')}`, {
7634
+ color: 'blue',
7635
+ });
7636
+ // Get the diff
7637
+ const diff = await git.diff(diffArgs);
7638
+ logger?.verbose(`Generated diff between "${headBranch}" and "${baseBranch}"`, {
7639
+ color: 'blue',
7640
+ });
7641
+ const changes = diff.split('diff --git').slice(1).map((fileDiff) => {
7642
+ const lines = fileDiff.split('\n');
7643
+ const filePathLine = lines[0];
7644
+ const filePath = filePathLine.split('b/')[1]?.split(' ')[0];
7645
+ const oldFilePath = filePathLine.split('a/')[1]?.split(' ')[0];
7646
+ // Determine status based on diff headers
7647
+ let status = 'modified';
7648
+ if (fileDiff.includes('new file mode')) {
7649
+ status = 'added';
7650
+ }
7651
+ else if (fileDiff.includes('deleted file mode')) {
7652
+ status = 'deleted';
7653
+ }
7654
+ else if (fileDiff.includes('rename from')) {
7655
+ status = 'renamed';
7656
+ }
7657
+ return {
7658
+ filePath: filePath || '',
7659
+ oldFilePath: oldFilePath || '',
7660
+ status,
7661
+ summary: getSummaryText({ path: filePath || '', index: '', working_dir: '' }, { filePath: filePath || '', status }),
7662
+ };
7663
+ });
7664
+ return {
7665
+ staged: changes,
7666
+ unstaged: [],
7667
+ untracked: [],
7668
+ };
7669
+ }
7670
+ catch (error) {
7671
+ const errorMessage = error instanceof Error ? error.message : String(error);
7672
+ console.error('Error in getDiffForBranch:', error);
7673
+ logger?.log(`Encountered an error getting diff between branches: ${errorMessage}`, { color: 'red' });
7674
+ logger?.log(`Branch details: baseBranch="${baseBranch}", headBranch="${headBranch}"`, { color: 'red' });
7675
+ // Re-throw the error so the caller can handle it appropriately
7676
+ throw error;
7677
+ }
7678
+ }
7679
+
7680
+ 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.
7486
7681
 
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"
7682
+ ## Input
7683
+ You will be provided with a summary of changes. This summary can be one of the following:
7684
+ 1. A list of commits, each with its author, hash, message, and body.
7685
+ 2. A list of commits, each with its details AND the full diff of the changes.
7686
+ 3. A single, comprehensive diff for an entire branch.
7492
7687
 
7688
+ ## Rules
7689
+ - Create a descriptive title for the changelog that gives a high-level overview of the changes.
7690
+ - **BREAKING CHANGES**: Identify any commits that introduce breaking changes. These must be listed first under a "### 💥 BREAKING CHANGES" heading.
7691
+ - **Grouping**: Logically group related changes under descriptive headings (e.g., ### Features, ### Fixes, ### Refactors).
7692
+ - **Dependencies**: Group all dependency updates (e.g., changes to package.json, go.mod) under a "### Dependencies" section.
7693
+ - **Summaries**: For each change, provide a concise summary.
7694
+ - **Attribution**: {{author_instructions}}
7695
+ - **Technical Details**: If provided with diffs, use them to understand the technical details and provide a more accurate and detailed description of the changes.
7696
+ - **Clarity**: Avoid generalizations like "various bug fixes," "improvements," or "enhancements." Be specific.
7697
+ - **Formatting**: Your entire response must be valid Markdown.
7698
+
7699
+ ## Formatting Instructions
7493
7700
  {{format_instructions}}
7494
7701
 
7702
+ {{additional_context}}
7703
+
7495
7704
  """{{summary}}"""`;
7496
- const inputVariables$2 = ['format_instructions', 'summary'];
7705
+ const inputVariables$2 = [
7706
+ 'format_instructions',
7707
+ 'summary',
7708
+ 'additional_context',
7709
+ 'author_instructions',
7710
+ ];
7497
7711
  const CHANGELOG_PROMPT = new prompts$1.PromptTemplate({
7498
7712
  template: template$3,
7499
7713
  inputVariables: inputVariables$2,
@@ -7517,48 +7731,68 @@ const handler$4 = async (argv, logger) => {
7517
7731
  }
7518
7732
  async function factory() {
7519
7733
  const branchName = await getCurrentBranchName({ git });
7520
- if (config.sinceLastTag) {
7521
- logger.verbose(`Generating commit log since the last tag`, { color: 'yellow' });
7734
+ if (argv.onlyDiff) {
7735
+ logger.verbose(`Generating changelog based on branch diff`, { color: 'yellow' });
7736
+ const diff = await getDiffForBranch({ git, logger, baseBranch: argv.branch || 'main', headBranch: branchName });
7522
7737
  return {
7523
7738
  branch: branchName,
7524
- commits: await getChangesSinceLastTag({ git, logger }),
7739
+ diff: JSON.stringify(diff.staged, null, 2),
7525
7740
  };
7526
7741
  }
7527
- if (config.range && config.range.includes(':')) {
7742
+ let commits = [];
7743
+ if (config.sinceLastTag) {
7744
+ logger.verbose(`Generating commit log since the last tag`, { color: 'yellow' });
7745
+ // This function returns string[], needs to be adapted or replaced
7746
+ // For now, this path will have limited details.
7747
+ const commitMessages = await getChangesSinceLastTag({ git, logger });
7748
+ commits = commitMessages.map(msg => ({ message: msg }));
7749
+ }
7750
+ else if (config.range && config.range.includes(':')) {
7528
7751
  const [from, to] = config.range.split(':');
7529
7752
  if (!from || !to) {
7530
7753
  logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
7531
7754
  process.exit(1);
7532
7755
  }
7533
- return {
7534
- branch: branchName,
7535
- commits: await getCommitLogRange(from, to, { git, noMerges: true }),
7536
- };
7756
+ commits = await getCommitLogRangeDetails(from, to, { git, noMerges: true });
7537
7757
  }
7538
- if (argv.branch) {
7758
+ else if (argv.branch) {
7539
7759
  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
- };
7760
+ commits = await getCommitLogAgainstBranch({ git, logger, targetBranch: argv.branch });
7761
+ }
7762
+ else {
7763
+ logger.verbose(`No range, branch, or tag option provided. Defaulting to current branch`, {
7764
+ color: 'yellow',
7765
+ });
7766
+ commits = await getCommitLogCurrentBranch({ git, logger });
7767
+ }
7768
+ let commitsWithDiffText = commits;
7769
+ if (argv.withDiff) {
7770
+ commitsWithDiffText = await Promise.all(commits.map(async (commit) => ({
7771
+ ...commit,
7772
+ diffText: await getDiffForCommit(commit.hash, { git }),
7773
+ })));
7544
7774
  }
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
7775
  return {
7550
7776
  branch: branchName,
7551
- commits,
7777
+ commits: commitsWithDiffText,
7778
+ withDiff: argv.withDiff,
7552
7779
  };
7553
7780
  }
7554
- async function parser({ branch, commits }) {
7555
- let result;
7556
- if (!commits || commits.length === 0) {
7557
- result = `## ${branch}\n\nNo commits found.`;
7781
+ async function parser(data) {
7782
+ if (data.diff) {
7783
+ return `## Diff for ${data.branch}\n\n${data.diff}`;
7558
7784
  }
7559
- else {
7560
- result = `## ${branch}\n\n${commits.map((commit) => commit.trim()).join('\n\n')}`;
7785
+ if (!data.commits || data.commits.length === 0) {
7786
+ return `## ${data.branch}\n\nNo commits found.`;
7561
7787
  }
7788
+ let result = `## ${data.branch}\n\n`;
7789
+ result += data.commits.map(commit => {
7790
+ let commitStr = `Author: ${commit.author_name}\nCommit: ${commit.hash}\nMessage: ${commit.message}\n${commit.body}`;
7791
+ if (data.withDiff && commit.diffText) {
7792
+ commitStr += `\nDiff:\n${commit.diffText}`;
7793
+ }
7794
+ return commitStr.trim();
7795
+ }).join('\n\n---\n\n');
7562
7796
  return result;
7563
7797
  }
7564
7798
  const changelogMsg = await generateAndReviewLoop({
@@ -7582,12 +7816,21 @@ const handler$4 = async (argv, logger) => {
7582
7816
  fallback: CHANGELOG_PROMPT,
7583
7817
  });
7584
7818
  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.";
7819
+ let additional_context = '';
7820
+ if (argv.additional) {
7821
+ additional_context = `## Additional Context\n${argv.additional}`;
7822
+ }
7823
+ const author_instructions = argv.author
7824
+ ? '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.'
7825
+ : '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
7826
  const changelog = await executeChain({
7586
7827
  llm,
7587
7828
  prompt,
7588
7829
  variables: {
7589
7830
  summary: context,
7590
7831
  format_instructions: formatInstructions,
7832
+ additional_context: additional_context,
7833
+ author_instructions: author_instructions,
7591
7834
  },
7592
7835
  parser,
7593
7836
  });
@@ -10899,76 +11142,6 @@ async function createCommit(message, git) {
10899
11142
  return await git.commit(message);
10900
11143
  }
10901
11144
 
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
11145
  /**
10973
11146
  * Retrieves the changes in the Git repository.
10974
11147
  *
@@ -11286,7 +11459,7 @@ const handler$3 = async (argv, logger) => {
11286
11459
  const schema = USE_CONVENTIONAL_COMMITS
11287
11460
  ? ConventionalCommitMessageResponseSchema
11288
11461
  : CommitMessageResponseSchema;
11289
- const formatInstructions = `You must always return valid JSON fenced by a markdown code block. Do not return any additional text. The JSON object you return should match the following schema:
11462
+ const formatInstructions = `You must always return a valid JSON object. Do not return any additional text. The JSON object you return should match the following schema:
11290
11463
  ${schema.description}
11291
11464
  {
11292
11465
  "title": "The commit title",
@@ -12006,86 +12179,6 @@ const getChangesByTimestamp = async ({ since, git }) => {
12006
12179
  return formatCommitLog(commitLog);
12007
12180
  };
12008
12181
 
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
12182
  async function noResult$1({ logger }) {
12090
12183
  logger.log('No repo changes detected. 👀', { color: 'blue' });
12091
12184
  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.1",
4
4
  "description": "zero-effort git commits with coco.",
5
5
  "author": "gfargo <ghfargo@gmail.com>",
6
6
  "license": "MIT",