DEV Community

Cover image for AI-Powered Commit Message Generator with Sring Boot & Cerebras
Deividas Strole
Deividas Strole

Posted on

AI-Powered Commit Message Generator with Sring Boot & Cerebras

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:

  1. A simple Spring Boot REST API
  2. Integration with Cerebras Cloud API
  3. 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

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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."
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

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!");
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

Top comments (0)