feat: support Conventional Commits via --type flag (#177)

Co-authored-by: Hiroki Osame <hiroki.osame@gmail.com>
This commit is contained in:
Thijs Limmen
2023-05-03 15:17:09 +02:00
committed by GitHub
parent f466f0527e
commit 0562761dc2
22 changed files with 723 additions and 11 deletions

View File

@@ -35,6 +35,11 @@ cli(
alias: 'a', alias: 'a',
default: false, default: false,
}, },
type: {
type: String,
description: 'Type of commit message to generate',
alias: 't',
},
}, },
commands: [ commands: [
@@ -56,6 +61,7 @@ cli(
argv.flags.generate, argv.flags.generate,
argv.flags.exclude, argv.flags.exclude,
argv.flags.all, argv.flags.all,
argv.flags.type,
rawArgv, rawArgv,
); );
} }

View File

@@ -18,6 +18,7 @@ export default async (
generate: number | undefined, generate: number | undefined,
excludeFiles: string[], excludeFiles: string[],
stageAll: boolean, stageAll: boolean,
commitType: string | undefined,
rawArgv: string[], rawArgv: string[],
) => (async () => { ) => (async () => {
intro(bgCyan(black(' aicommits '))); intro(bgCyan(black(' aicommits ')));
@@ -45,6 +46,7 @@ export default async (
OPENAI_KEY: env.OPENAI_KEY || env.OPENAI_API_KEY, OPENAI_KEY: env.OPENAI_KEY || env.OPENAI_API_KEY,
proxy: env.https_proxy || env.HTTPS_PROXY || env.http_proxy || env.HTTP_PROXY, proxy: env.https_proxy || env.HTTPS_PROXY || env.http_proxy || env.HTTP_PROXY,
generate: generate?.toString(), generate: generate?.toString(),
type: commitType?.toString(),
}); });
const s = spinner(); const s = spinner();
@@ -58,6 +60,7 @@ export default async (
staged.diff, staged.diff,
config.generate, config.generate,
config['max-length'], config['max-length'],
config.type,
config.timeout, config.timeout,
config.proxy, config.proxy,
); );

View File

@@ -46,6 +46,7 @@ export default () => (async () => {
staged!.diff, staged!.diff,
config.generate, config.generate,
config['max-length'], config['max-length'],
config.type,
config.timeout, config.timeout,
config.proxy, config.proxy,
); );

View File

@@ -6,6 +6,10 @@ import type { TiktokenModel } from '@dqbd/tiktoken';
import { fileExists } from './fs.js'; import { fileExists } from './fs.js';
import { KnownError } from './error.js'; import { KnownError } from './error.js';
const commitTypes = ['', 'conventional'] as const;
export type CommitType = typeof commitTypes[number];
const { hasOwnProperty } = Object.prototype; const { hasOwnProperty } = Object.prototype;
export const hasOwn = (object: unknown, key: PropertyKey) => hasOwnProperty.call(object, key); export const hasOwn = (object: unknown, key: PropertyKey) => hasOwnProperty.call(object, key);
@@ -51,6 +55,15 @@ const configParsers = {
return parsed; return parsed;
}, },
type(type?: string) {
if (!type) {
return '';
}
parseAssert('type', commitTypes.includes(type as CommitType), 'Invalid commit type');
return type as CommitType;
},
proxy(url?: string) { proxy(url?: string) {
if (!url || url.length === 0) { if (!url || url.length === 0) {
return undefined; return undefined;
@@ -99,7 +112,7 @@ type RawConfig = {
[key in ConfigKeys]?: string; [key in ConfigKeys]?: string;
}; };
type ValidConfig = { export type ValidConfig = {
[Key in ConfigKeys]: ReturnType<typeof configParsers[Key]>; [Key in ConfigKeys]: ReturnType<typeof configParsers[Key]>;
}; };

View File

@@ -1,6 +1,6 @@
import https from 'https'; import https from 'https';
import type { ClientRequest, IncomingMessage } from 'http'; import type { ClientRequest, IncomingMessage } from 'http';
import type { CreateChatCompletionRequest, CreateChatCompletionResponse } from 'openai'; import type { ChatCompletionRequestMessage, CreateChatCompletionRequest, CreateChatCompletionResponse } from 'openai';
import { import {
TiktokenModel, TiktokenModel,
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
@@ -8,6 +8,7 @@ import {
} from '@dqbd/tiktoken'; } from '@dqbd/tiktoken';
import createHttpsProxyAgent from 'https-proxy-agent'; import createHttpsProxyAgent from 'https-proxy-agent';
import { KnownError } from './error.js'; import { KnownError } from './error.js';
import type { CommitType } from './config.js';
const httpsPost = async ( const httpsPost = async (
hostname: string, hostname: string,
@@ -104,16 +105,51 @@ const sanitizeMessage = (message: string) => message.trim().replace(/[\n\r]/g, '
const deduplicateMessages = (array: string[]) => Array.from(new Set(array)); const deduplicateMessages = (array: string[]) => Array.from(new Set(array));
const getPrompt = ( const getBasePrompt = (
locale: string, locale: string,
diff: string,
maxLength: number, maxLength: number,
) => `${[ ) => `${[
'Generate a concise git commit message written in present tense for the following code diff with the given specifications below:', 'Generate a concise git commit message written in present tense for the following code diff with the given specifications below:',
`Message language: ${locale}`, `Message language: ${locale}`,
`Commit message must be a maximum of ${maxLength} characters.`, `Commit message must be a maximum of ${maxLength} characters.`,
'Exclude anything unnecessary such as translation. Your entire response will be passed directly into git commit.', 'Exclude anything unnecessary such as translation. Your entire response will be passed directly into git commit.',
].join('\n')}\n\n${diff}`; ].join('\n')}`;
const getCommitMessageFormatOutputExample = (type: CommitType) => `The output response must be in format:\n${getCommitMessageFormat(type)}`;
const getCommitMessageFormat = (type: CommitType) => {
if (type === 'conventional') {
return '<type>(<optional scope>): <commit message>';
}
return '<commit message>';
};
/**
* References:
* Commitlint:
* https://github.com/conventional-changelog/commitlint/blob/18fbed7ea86ac0ec9d5449b4979b762ec4305a92/%40commitlint/config-conventional/index.js#L40-L100
*
* Conventional Changelog:
* https://github.com/conventional-changelog/conventional-changelog/blob/d0e5d5926c8addba74bc962553dd8bcfba90e228/packages/conventional-changelog-conventionalcommits/writer-opts.js#L182-L193
*/
const getExtraContextForConventionalCommits = () => (
`Choose a type from the type-to-description JSON below that best describes the git diff:\n${
JSON.stringify({
docs: 'Documentation only changes',
style: 'Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)',
refactor: 'A code change that neither fixes a bug nor adds a feature',
perf: 'A code change that improves performance',
test: 'Adding missing tests or correcting existing tests',
build: 'Changes that affect the build system or external dependencies',
ci: 'Changes to our CI configuration files and scripts',
chore: "Other changes that don't modify src or test files",
revert: 'Reverts a previous commit',
feat: 'A new feature',
fix: 'A bug fix',
}, null, 2)
}`
);
const generateStringFromLength = (length: number) => { const generateStringFromLength = (length: number) => {
let result = ''; let result = '';
@@ -139,10 +175,28 @@ export const generateCommitMessage = async (
diff: string, diff: string,
completions: number, completions: number,
maxLength: number, maxLength: number,
type: CommitType,
timeout: number, timeout: number,
proxy?: string, proxy?: string,
) => { ) => {
const prompt = getPrompt(locale, diff, maxLength); const prompt = getBasePrompt(locale, maxLength);
const conventionalCommitsExtraContext = type === 'conventional'
? getExtraContextForConventionalCommits()
: '';
const commitMessageFormatOutputExample = getCommitMessageFormatOutputExample(type);
const messages: ChatCompletionRequestMessage[] = [
{
role: 'system',
content: `${prompt}\n${conventionalCommitsExtraContext}\n${commitMessageFormatOutputExample}`,
},
{
role: 'user',
content: diff,
},
];
// Padded by 5 for more room for the completion. // Padded by 5 for more room for the completion.
const stringFromLength = generateStringFromLength(maxLength + 5); const stringFromLength = generateStringFromLength(maxLength + 5);
@@ -155,10 +209,7 @@ export const generateCommitMessage = async (
apiKey, apiKey,
{ {
model, model,
messages: [{ messages,
role: 'user',
content: prompt,
}],
temperature: 0.7, temperature: 0.7,
top_p: 1, top_p: 1,
frequency_penalty: 0, frequency_penalty: 0,

View File

@@ -2,6 +2,7 @@ import { describe } from 'manten';
describe('aicommits', ({ runTestSuite }) => { describe('aicommits', ({ runTestSuite }) => {
runTestSuite(import('./specs/cli/index.js')); runTestSuite(import('./specs/cli/index.js'));
runTestSuite(import('./specs/openai/index.js'));
runTestSuite(import('./specs/config.js')); runTestSuite(import('./specs/config.js'));
runTestSuite(import('./specs/git-hook.js')); runTestSuite(import('./specs/git-hook.js'));
}); });

View File

@@ -86,7 +86,7 @@ export default testSuite(({ describe }) => {
commitMessage, commitMessage,
length: commitMessage.length, length: commitMessage.length,
}); });
expect(commitMessage.length <= 20).toBe(true); expect(commitMessage.length).toBeLessThanOrEqual(20);
await fixture.rm(); await fixture.rm();
}); });
@@ -208,6 +208,140 @@ export default testSuite(({ describe }) => {
await fixture.rm(); await fixture.rm();
}); });
describe('commit types', ({ test }) => {
test('Should not use conventional commits by default', async () => {
const conventionalCommitPattern = /(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test):\s/;
const { fixture, aicommits } = await createFixture({
...files,
});
const git = await createGit(fixture.path);
await git('add', ['data.json']);
const committing = aicommits();
committing.stdout!.on('data', (buffer: Buffer) => {
const stdout = buffer.toString();
if (stdout.match('└')) {
committing.stdin!.write('y');
committing.stdin!.end();
}
});
await committing;
const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']);
expect(statusAfter.stdout).toBe('');
const { stdout: commitMessage } = await git('log', ['--oneline']);
console.log('Committed with:', commitMessage);
expect(commitMessage).not.toMatch(conventionalCommitPattern);
await fixture.rm();
});
test('Conventional commits', async () => {
const conventionalCommitPattern = /(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test):\s/;
const { fixture, aicommits } = await createFixture({
...files,
'.aicommits': `${files['.aicommits']}\ntype=conventional`,
});
const git = await createGit(fixture.path);
await git('add', ['data.json']);
const committing = aicommits();
committing.stdout!.on('data', (buffer: Buffer) => {
const stdout = buffer.toString();
if (stdout.match('└')) {
committing.stdin!.write('y');
committing.stdin!.end();
}
});
await committing;
const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']);
expect(statusAfter.stdout).toBe('');
const { stdout: commitMessage } = await git('log', ['--oneline']);
console.log('Committed with:', commitMessage);
expect(commitMessage).toMatch(conventionalCommitPattern);
await fixture.rm();
});
test('Accepts --type flag, overriding config', async () => {
const conventionalCommitPattern = /(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test):\s/;
const { fixture, aicommits } = await createFixture({
...files,
'.aicommits': `${files['.aicommits']}\ntype=other`,
});
const git = await createGit(fixture.path);
await git('add', ['data.json']);
// Generate flag should override generate config
const committing = aicommits([
'--type', 'conventional',
]);
committing.stdout!.on('data', (buffer: Buffer) => {
const stdout = buffer.toString();
if (stdout.match('└')) {
committing.stdin!.write('y');
committing.stdin!.end();
}
});
await committing;
const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']);
expect(statusAfter.stdout).toBe('');
const { stdout: commitMessage } = await git('log', ['--oneline']);
console.log('Committed with:', commitMessage);
expect(commitMessage).toMatch(conventionalCommitPattern);
await fixture.rm();
});
test('Accepts empty --type flag', async () => {
const conventionalCommitPattern = /(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test):\s/;
const { fixture, aicommits } = await createFixture({
...files,
'.aicommits': `${files['.aicommits']}\ntype=conventional`,
});
const git = await createGit(fixture.path);
await git('add', ['data.json']);
const committing = aicommits([
'--type', '',
]);
committing.stdout!.on('data', (buffer: Buffer) => {
const stdout = buffer.toString();
if (stdout.match('└')) {
committing.stdin!.write('y');
committing.stdin!.end();
}
});
await committing;
const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']);
expect(statusAfter.stdout).toBe('');
const { stdout: commitMessage } = await git('log', ['--oneline']);
console.log('Committed with:', commitMessage);
expect(commitMessage).not.toMatch(conventionalCommitPattern);
await fixture.rm();
});
});
describe('proxy', ({ test }) => { describe('proxy', ({ test }) => {
test('Fails on invalid proxy', async () => { test('Fails on invalid proxy', async () => {
const { fixture, aicommits } = await createFixture({ const { fixture, aicommits } = await createFixture({

View File

@@ -0,0 +1,161 @@
import { readFile } from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { expect, testSuite } from 'manten';
import {
generateCommitMessage,
} from '../../../src/utils/openai.js';
import type { ValidConfig } from '../../../src/utils/config.js';
const { OPENAI_KEY } = process.env;
export default testSuite(({ describe }) => {
if (!OPENAI_KEY) {
console.warn('⚠️ process.env.OPENAI_KEY is necessary to run these tests. Skipping...');
return;
}
describe('Conventional Commits', async ({ test }) => {
await test('Should not translate conventional commit type to Japanase when locale config is set to japanese', async () => {
const japaneseConventionalCommitPattern = /(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(.*\))?: [\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\uFF00-\uFF9F\u4E00-\u9FAF\u3400-\u4DBF]/;
const gitDiff = await readDiffFromFile('new-feature.txt');
const commitMessage = await runGenerateCommitMessage(gitDiff, {
locale: 'ja',
});
expect(commitMessage).toMatch(japaneseConventionalCommitPattern);
console.log('Generated message:', commitMessage);
});
await test('Should use "feat:" conventional commit when change relate to adding a new feature', async () => {
const gitDiff = await readDiffFromFile('new-feature.txt');
const commitMessage = await runGenerateCommitMessage(gitDiff);
// should match "feat:" or "feat(<scope>):"
expect(commitMessage).toMatch(/(feat(\(.*\))?):/);
console.log('Generated message:', commitMessage);
});
await test('Should use "refactor:" conventional commit when change relate to code refactoring', async () => {
const gitDiff = await readDiffFromFile('code-refactoring.txt');
const commitMessage = await runGenerateCommitMessage(gitDiff);
// should match "refactor:" or "refactor(<scope>):"
expect(commitMessage).toMatch(/(refactor(\(.*\))?):/);
console.log('Generated message:', commitMessage);
});
await test('Should use "test:" conventional commit when change relate to testing a React application', async () => {
const gitDiff = await readDiffFromFile('testing-react-application.txt');
const commitMessage = await runGenerateCommitMessage(gitDiff);
// should match "test:" or "test(<scope>):"
expect(commitMessage).toMatch(/(test(\(.*\))?):/);
console.log('Generated message:', commitMessage);
});
await test('Should use "build:" conventional commit when change relate to github action build pipeline', async () => {
const gitDiff = await readDiffFromFile(
'github-action-build-pipeline.txt',
);
const commitMessage = await runGenerateCommitMessage(gitDiff);
// should match "build:" or "build(<scope>):"
expect(commitMessage).toMatch(/((build|ci)(\(.*\))?):/);
console.log('Generated message:', commitMessage);
});
await test('Should use "(ci|build):" conventional commit when change relate to continious integration', async () => {
const gitDiff = await readDiffFromFile('continous-integration.txt');
const commitMessage = await runGenerateCommitMessage(gitDiff);
// should match "ci:" or "ci(<scope>):
// It also sometimes generates build and feat
expect(commitMessage).toMatch(/((ci|build|feat)(\(.*\))?):/);
console.log('Generated message:', commitMessage);
});
await test('Should use "docs:" conventional commit when change relate to documentation changes', async () => {
const gitDiff = await readDiffFromFile('documentation-changes.txt');
const commitMessage = await runGenerateCommitMessage(gitDiff);
// should match "docs:" or "docs(<scope>):"
expect(commitMessage).toMatch(/(docs(\(.*\))?):/);
console.log('Generated message:', commitMessage);
});
await test('Should use "fix:" conventional commit when change relate to fixing code', async () => {
const gitDiff = await readDiffFromFile('fix-nullpointer-exception.txt');
const commitMessage = await runGenerateCommitMessage(gitDiff);
// should match "fix:" or "fix(<scope>):"
// Sometimes it generates refactor
expect(commitMessage).toMatch(/((fix|refactor)(\(.*\))?):/);
console.log('Generated message:', commitMessage);
});
await test('Should use "style:" conventional commit when change relate to code style improvements', async () => {
const gitDiff = await readDiffFromFile('code-style.txt');
const commitMessage = await runGenerateCommitMessage(gitDiff);
// should match "style:" or "style(<style>):"
expect(commitMessage).toMatch(/(style|refactor|fix)(\(.*\))?:/);
console.log('Generated message:', commitMessage);
});
await test('Should use "chore:" conventional commit when change relate to a chore or maintenance', async () => {
const gitDiff = await readDiffFromFile('chore.txt');
const commitMessage = await runGenerateCommitMessage(gitDiff);
// should match "chore:" or "chore(<style>):"
// Sometimes it generates build|feat
expect(commitMessage).toMatch(/((chore|build|feat)(\(.*\))?):/);
console.log('Generated message:', commitMessage);
});
await test('Should use "perf:" conventional commit when change relate to a performance improvement', async () => {
const gitDiff = await readDiffFromFile('performance-improvement.txt');
const commitMessage = await runGenerateCommitMessage(gitDiff);
// should match "perf:" or "perf(<style>):"
// It also sometimes generates refactor:
expect(commitMessage).toMatch(/((perf|refactor)(\(.*\))?):/);
console.log('Generated message:', commitMessage);
});
async function runGenerateCommitMessage(gitDiff: string,
configOverrides: Partial<ValidConfig> = {}): Promise<string> {
const config = {
locale: 'en',
type: 'conventional',
generate: 1,
'max-length': 50,
...configOverrides,
} as ValidConfig;
const commitMessages = await generateCommitMessage(OPENAI_KEY!, 'gpt-3.5-turbo', config.locale, gitDiff, config.generate, config['max-length'], config.type, 7000);
return commitMessages[0];
}
/*
* See ./diffs/README.md in order to generate diff files
*/
async function readDiffFromFile(filename: string): Promise<string> {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const gitDiff = await readFile(
path.resolve(__dirname, `./diff-fixtures/${filename}`),
'utf8',
);
return gitDiff;
}
});
});

View File

@@ -0,0 +1,11 @@
# Generating diffs
1. Instruct ChatGPT with the following command:
```
I want you to act as a git cli
I will give you the type of content and you will generate a random git diff based on that
```
2. Insert the type of change
ChatGPT will generate a fictional git diff based on the type of change you inserted.

View File

@@ -0,0 +1,18 @@
diff --git a/package.json b/package.json
index 2a7398e..6b2a3f0 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"version": "1.0.0",
"description": "A sample project",
"main": "index.js",
- "scripts": {
+ "scripts": {
"start": "node index.js",
"test": "mocha",
- "lint": "eslint ."
+ "lint": "eslint .",
+ "clean": "rm -rf node_modules && npm install"
},
"dependencies": {
"express": "^4.17.1",

View File

@@ -0,0 +1,34 @@
diff --git a/old_example.ts b/new_example.ts
index 1234567..abcdefg 100644
--- a/old_example.ts
+++ b/new_example.ts
@@ -1,15 +1,16 @@
-import { Component, OnInit } from '@angular/core';
+import { Component } from '@angular/core';
-@Component({
- selector: 'app-example',
- templateUrl: './example.component.html',
- styleUrls: ['./example.component.css']
-})
-export class ExampleComponent implements OnInit {
- message: string;
+@Component({
+ selector: 'app-improved-example',
+ templateUrl: './improved-example.component.html',
+ styleUrls: ['./improved-example.component.css']
+})
+export class ImprovedExampleComponent {
+ private _message: string;
- ngOnInit() {
- this.message = 'Hello, world!';
+ constructor() {
+ this._message = 'Hello, world!';
}
+ get message(): string {
+ return this._message;
+ }
}

View File

@@ -0,0 +1,28 @@
diff --git a/src/app.js b/src/app.js
index 8741c37..91b2e74 100644
--- a/src/app.js
+++ b/src/app.js
@@ -10,12 +10,12 @@ app.use(express.json());
// Routes
app.get('/', (req, res) => {
- res.send('Welcome to the API!');
+ res.send('Welcome to the API!');
});
app.post('/users', (req, res) => {
- const user = createUser(req.body);
- res.status(201).send(user);
+ const user = createUser(req.body);
+ res.status(201).send(user);
});
app.get('/users/:id', (req, res) => {
@@ -27,7 +27,7 @@ app.get('/users/:id', (req, res) => {
if (user) {
res.send(user);
} else {
- res.status(404).send('User not found');
+ res.status(404).send('User not found');
}
});

View File

@@ -0,0 +1,30 @@
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..b6e5789
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,16 @@
+name: Continuous Integration
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ build-and-test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ - name: Set up Node.js
+ uses: actions/setup-node@v2
+ with:
+ node-version: '16'
+ - name: Install dependencies
+ run: npm ci
+ - name: Run tests
+ run: npm test

View File

@@ -0,0 +1,27 @@
diff --git a/old_feature.py b/old_feature.py
index 1234567..abcdefg 100644
--- a/old_feature.py
+++ b/old_feature.py
@@ -1,7 +1,9 @@
import warnings
class OldFeature:
+ def __init__(self):
+ warnings.warn("OldFeature is deprecated and will be removed in the next release. Please use NewFeature instead.", DeprecationWarning)
def do_something(self):
print("Doing something with the old feature...")
diff --git a/new_feature.py b/new_feature.py
new file mode 100644
index 0000000..1111111
--- /dev/null
+++ b/new_feature.py
@@ -0,0 +1,7 @@
+class NewFeature:
+ def __init__(self):
+ print("Initializing the new feature...")
+
+ def do_something(self):
+ print("Doing something with the new feature...")
+

View File

@@ -0,0 +1,30 @@
diff --git a/README.md b/README.md
index a0c3e1b..9d1b6f8 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,11 @@
# My Awesome Project
+## Overview
+
+My Awesome Project is a web application that allows users to manage their tasks and projects in a simple and intuitive way. The project is built with React and Node.js and uses MongoDB for data storage.
+
## Installation
+To install and run My Awesome Project, follow these steps:
+
1. Clone the repository: `git clone https://github.com/username/my-awesome-project.git`
2. Install dependencies: `npm install`
3. Start the development server: `npm start`
@@ -13,6 +18,11 @@ To install and run My Awesome Project, follow these steps:
## Usage
To use My Awesome Project, follow these steps:
+
+1. Open your web browser and navigate to `http://localhost:3000`
+2. Sign up for a new account or log in to an existing one
+3. Create a new task or project and start managing your work!
+
## Contributing
We welcome contributions from anyone and everyone. To contribute to My Awesome Project, follow these steps:

View File

@@ -0,0 +1,14 @@
diff --git a/src/main/java/com/example/MyClass.java b/src/main/java/com/example/MyClass.java
index e7d8f38..caab7f1 100644
--- a/src/main/java/com/example/MyClass.java
+++ b/src/main/java/com/example/MyClass.java
@@ -23,7 +23,10 @@ public class MyClass {
public void processItems(List<Item> items) {
for (Item item : items) {
- if (item.getValue().equalsIgnoreCase("example")) {
+ // Fixing NullPointerException by adding a null check
+ String itemValue = item.getValue();
+ if (itemValue != null && itemValue.equalsIgnoreCase("example")) {
processExampleItem(item);
}
}

View File

@@ -0,0 +1,21 @@
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 1d07d31..085eb64 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -10,6 +10,8 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: 12.x
+ - name: Install dependencies
+ run: npm install
- name: Build and test
run: |
npm run build
@@ -22,3 +24,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v1
with:
+ name: Build artifact
+ path: build
+ - name: Deploy to production
+ uses: some-third-party/deploy-action@v1

View File

@@ -0,0 +1,47 @@
diff --git a/src/features/newFeature.js b/src/features/newFeature.js
new file mode 100644
index 0000000..b6e5789
--- /dev/null
+++ b/src/features/newFeature.js
@@ -0,0 +1,18 @@
+/**
+ * New feature: Calculates the factorial of a given number.
+ * @param {number} n - The input number.
+ * @returns {number} - The factorial of the input number.
+ */
+function factorial(n) {
+ if (n === 0 || n === 1) {
+ return 1;
+ }
+ return n * factorial(n - 1);
+}
+
+module.exports = {
+ factorial,
+};
+
diff --git a/src/app.js b/src/app.js
index 8741c37..91b2e74 100644
--- a/src/app.js
+++ b/src/app.js
@@ -2,6 +2,7 @@
const express = require('express');
const bodyParser = require('body-parser');
const userRoutes = require('./routes/userRoutes');
+const { factorial } = require('./features/newFeature');
const app = express();
app.use(bodyParser.json());
@@ -21,6 +22,12 @@
res.send('Welcome to the API!');
});
+app.get('/factorial/:number', (req, res) => {
+ const number = parseInt(req.params.number, 10);
+ const result = factorial(number);
+ res.send(`Factorial of ${number} is ${result}`);
+});
+
// Other routes...
module.exports = app;

View File

@@ -0,0 +1,26 @@
diff --git a/src/loop.js b/src/loop.js
index 1d45a2b..8c52e81 100644
--- a/src/loop.js
+++ b/src/loop.js
@@ -5,14 +5,14 @@ const items = generateItems(100000);
function processData(items) {
let sum = 0;
- for (let i = 0; i < items.length; i++) {
- const item = items[i];
- if (item.isValid()) {
- sum += item.value;
- }
+ for (const item of items) {
+ if (item.isValid()) sum += item.value;
}
return sum;
}
const startTime = Date.now();
-const result = processData(items);
+const result = processData(items); // Improved loop iteration
const endTime = Date.now();
console.log(`Result: ${result}, Time: ${endTime - startTime} ms`);

View File

@@ -0,0 +1,27 @@
diff --git a/Controllers/FeatureController.cs b/Controllers/FeatureController.cs
index 8a3b7c1..3e29f9a 100644
--- a/Controllers/FeatureController.cs
+++ b/Controllers/FeatureController.cs
@@ -1,16 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
namespace MyWebApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class FeatureController : ControllerBase
{
- [HttpGet("old-feature")]
- public ActionResult<string> GetOldFeature()
- {
- return "This is the removed old feature.";
- }
-
[HttpGet("new-feature")]
public ActionResult<string> GetNewFeature()
{
return "This is the new feature.";
}
}
}

View File

@@ -0,0 +1,22 @@
diff --git a/src/components/MyComponent.test.js b/src/components/MyComponent.test.js
index 37eabf2..976c6bf 100644
--- a/src/components/MyComponent.test.js
+++ b/src/components/MyComponent.test.js
@@ -10,6 +10,7 @@ describe("MyComponent", () => {
});
it("renders the component correctly", () => {
+ const props = { name: "John Doe", age: 25 };
const tree = renderer.create(<MyComponent {...props} />).toJSON();
expect(tree).toMatchSnapshot();
});
@@ -25,6 +26,11 @@ describe("MyComponent", () => {
expect(wrapper.find("h1").text()).toEqual("Hello, John Doe!");
});
+ it("displays the correct age", () => {
+ const props = { name: "Jane Doe", age: 30 };
+ const wrapper = shallow(<MyComponent {...props} />);
+ expect(wrapper.find("p").text()).toEqual("Age: 30");
+ });
});

View File

@@ -0,0 +1,7 @@
import { testSuite } from 'manten';
export default testSuite(({ describe }) => {
describe('OpenAI', ({ runTestSuite }) => {
runTestSuite(import('./conventional-commits.js'));
});
});