I don't know anyone who likes writing proper commit messages. Everyone knows they have to write them, but most are too lazy to think a good message and compose one. I, for example, am so lazy that even for 'work in progress' I just type 'wip'... In this tutorial we will create a REST API which will take your git diff (differences between versions of your code) and automatically generate a professional commit message. Magic!
Our battle plan:
- A simple Spring Boot REST API
- Integration with Cerebras Cloud API
- One endpoint: send a diff, get a formatted commit message
Why Cerebras?
They give free API keys if you ask nicely! Shhh... it's a secret! Also—their API is OpenAI-compatible! ;)
Prerequisites
- Java 17+
- Maven
- A Cerebras API key (get free credits at https://cloud.cerebras.ai)
- Basic Spring Boot knowledge
Project Setup
1. Create Spring Boot Project
Using Spring Initializr, create a project with these dependencies:
- Spring Web
- Spring WebFlux
- Lombok
- Validation
Or use this pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>commit-message-generator</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. Application Configuration
Create src/main/resources/application.properties:
# Cerebras API Configuration
cerebras.api-key=${CEREBRAS_API_KEY}
cerebras.api-url=https://api.cerebras.ai/v1/chat/completions
cerebras.model=llama3.1-70b
# Server Configuration
server.port=8080
# Logging
logging.level.com.example.commitgen=INFO
Building the Application
1. Configuration Class
Create src/main/java/com/example/commitgen/config/CerebrasConfig.java:
package com.example.commitgen.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import lombok.Data;
@Configuration
@ConfigurationProperties(prefix = "cerebras")
@Data
public class CerebrasConfig {
private String apiKey;
private String apiUrl;
private String model;
}
2. DTOs (Data Transfer Objects)
Create src/main/java/com/example/commitgen/dto/CommitRequest.java:
package com.example.commitgen.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class CommitRequest {
@NotBlank(message = "Diff cannot be empty")
private String diff;
private String type; // feat, fix, docs, etc. (optional)
private String scope; // Optional scope
}
Create src/main/java/com/example/commitgen/dto/CommitResponse.java:
package com.example.commitgen.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommitResponse {
private String message;
private String type;
private String scope;
private String description;
private String body;
}
Create Cerebras API DTOs in src/main/java/com/example/commitgen/dto/CerebrasRequest.java:
package com.example.commitgen.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.List;
@Data
@AllArgsConstructor
public class CerebrasRequest {
private String model;
private List<Message> messages;
@JsonProperty("max_tokens")
private Integer maxTokens;
private Double temperature;
@Data
@AllArgsConstructor
public static class Message {
private String role;
private String content;
}
}
Create src/main/java/com/example/commitgen/dto/CerebrasResponse.java:
package com.example.commitgen.dto;
import lombok.Data;
import java.util.List;
@Data
public class CerebrasResponse {
private List<Choice> choices;
@Data
public static class Choice {
private Message message;
@Data
public static class Message {
private String role;
private String content;
}
}
}
3. Cerebras API Service
Create src/main/java/com/example/commitgen/service/CerebrasService.java:
package com.example.commitgen.service;
import com.example.commitgen.config.CerebrasConfig;
import com.example.commitgen.dto.CerebrasRequest;
import com.example.commitgen.dto.CerebrasResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.List;
@Service
@RequiredArgsConstructor
@Slf4j
public class CerebrasService {
private final CerebrasConfig config;
private final WebClient.Builder webClientBuilder;
public Mono<String> generateCompletion(String prompt) {
WebClient webClient = webClientBuilder
.baseUrl(config.getApiUrl())
.defaultHeader("Authorization", "Bearer " + config.getApiKey())
.defaultHeader("Content-Type", "application/json")
.build();
CerebrasRequest request = new CerebrasRequest(
config.getModel(),
List.of(
new CerebrasRequest.Message("system",
"You are a helpful assistant that generates conventional commit messages."),
new CerebrasRequest.Message("user", prompt)
),
500,
0.3
);
return webClient.post()
.bodyValue(request)
.retrieve()
.bodyToMono(CerebrasResponse.class)
.map(response -> {
if (response.getChoices() != null && !response.getChoices().isEmpty()) {
return response.getChoices().get(0).getMessage().getContent();
}
throw new RuntimeException("No response from Cerebras API");
})
.doOnError(error -> log.error("Error calling Cerebras API", error));
}
}
4. Commit Message Service
Create src/main/java/com/example/commitgen/service/CommitMessageService.java:
package com.example.commitgen.service;
import com.example.commitgen.dto.CommitRequest;
import com.example.commitgen.dto.CommitResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
@Service
@RequiredArgsConstructor
@Slf4j
public class CommitMessageService {
private final CerebrasService cerebrasService;
public Mono<CommitResponse> generateCommitMessage(CommitRequest request) {
String prompt = buildPrompt(request);
return cerebrasService.generateCompletion(prompt)
.map(this::parseResponse)
.onErrorResume(error -> {
log.error("Error generating commit message", error);
return Mono.just(createErrorResponse());
});
}
private String buildPrompt(CommitRequest request) {
StringBuilder prompt = new StringBuilder();
prompt.append("Based on the following git diff, generate a conventional commit message.\n\n");
prompt.append("Format:\n");
prompt.append("<type>(<scope>): <description>\n\n");
prompt.append("<body>\n\n");
prompt.append("Where:\n");
prompt.append("- type: feat, fix, docs, style, refactor, test, chore\n");
prompt.append("- scope: optional, component/file affected\n");
prompt.append("- description: brief summary (50 chars max)\n");
prompt.append("- body: detailed explanation (optional)\n\n");
if (request.getType() != null) {
prompt.append("Preferred type: ").append(request.getType()).append("\n");
}
if (request.getScope() != null) {
prompt.append("Scope: ").append(request.getScope()).append("\n");
}
prompt.append("\nDiff:\n");
prompt.append(request.getDiff());
prompt.append("\n\n");
prompt.append("Return ONLY the commit message in the format specified above. ");
prompt.append("Do not include any explanations or additional text.");
return prompt.toString();
}
private CommitResponse parseResponse(String response) {
// Clean up the response
String cleaned = response.trim();
// Parse conventional commit format
String[] lines = cleaned.split("\n", 2);
String firstLine = lines[0].trim();
String body = lines.length > 1 ? lines[1].trim() : "";
// Extract type, scope, and description
String type = "";
String scope = "";
String description = firstLine;
// Parse: type(scope): description
if (firstLine.contains(":")) {
String[] parts = firstLine.split(":", 2);
String prefix = parts[0].trim();
description = parts.length > 1 ? parts[1].trim() : "";
if (prefix.contains("(") && prefix.contains(")")) {
int scopeStart = prefix.indexOf("(");
int scopeEnd = prefix.indexOf(")");
type = prefix.substring(0, scopeStart).trim();
scope = prefix.substring(scopeStart + 1, scopeEnd).trim();
} else {
type = prefix;
}
}
return new CommitResponse(
cleaned,
type,
scope,
description,
body
);
}
private CommitResponse createErrorResponse() {
return new CommitResponse(
"chore: update files",
"chore",
"",
"update files",
"Failed to generate AI-powered commit message. Using default."
);
}
}
5. REST Controller
Create src/main/java/com/example/commitgen/controller/CommitController.java:
package com.example.commitgen.controller;
import com.example.commitgen.dto.CommitRequest;
import com.example.commitgen.dto.CommitResponse;
import com.example.commitgen.service.CommitMessageService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/v1/commit")
@RequiredArgsConstructor
@CrossOrigin(origins = "*")
public class CommitController {
private final CommitMessageService commitMessageService;
@PostMapping("/generate")
public Mono<ResponseEntity<CommitResponse>> generateCommitMessage(
@Valid @RequestBody CommitRequest request) {
return commitMessageService.generateCommitMessage(request)
.map(ResponseEntity::ok);
}
@GetMapping("/health")
public ResponseEntity<String> health() {
return ResponseEntity.ok("Commit Message Generator is running!");
}
}
6. Main Application
Create src/main/java/com/example/commitgen/CommitMessageGeneratorApplication.java:
package com.example.commitgen;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.reactive.function.client.WebClient;
@SpringBootApplication
public class CommitMessageGeneratorApplication {
public static void main(String[] args) {
SpringApplication.run(CommitMessageGeneratorApplication.class, args);
}
@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}
Running the Application
1. Set Your API Key
export CEREBRAS_API_KEY=your-api-key-here
2. Start the Application
bashmvn spring-boot:run
Conclusion
You have built functional AI commit message generator in under 200 lines!!! Et voilà !
About the Author
Deividas Strole is a Full-Stack Developer based in California, specializing in Java, Spring Boot, React, and AI-driven development. He writes about software engineering, modern full-stack development, and digital marketing strategies.
Connect with me:
- Personal Website: https://DeividasStrole.com
- LinkedIn: https://linkedin.com/in/deividas-strole
- GitHub: https://github.com/deividas-strole
- YouTube: https://youtube.com/@deividas-strole
- Academia: https://national.academia.edu/DeividasStrole
- X: https://x.com/deividasstrole
Top comments (0)