diff --git a/README.md b/README.md
index f6a62ee5..4d1198c3 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,7 @@ Opensource Git GUI client.
* Git LFS
* Issue Link
* Workspace
+* Using AI to generate commit message (C# port of [anjerodev/commitollama](https://github.com/anjerodev/commitollama))
> [!WARNING]
> **Linux** only tested on **Debian 12** on both **X11** & **Wayland**.
diff --git a/src/App.JsonCodeGen.cs b/src/App.JsonCodeGen.cs
index 8daea4f9..70567af5 100644
--- a/src/App.JsonCodeGen.cs
+++ b/src/App.JsonCodeGen.cs
@@ -46,6 +46,8 @@ namespace SourceGit
[JsonSerializable(typeof(Models.ExternalToolPaths))]
[JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))]
[JsonSerializable(typeof(Models.JetBrainsState))]
+ [JsonSerializable(typeof(Models.OpenAIChatRequest))]
+ [JsonSerializable(typeof(Models.OpenAIChatResponse))]
[JsonSerializable(typeof(Models.ThemeOverrides))]
[JsonSerializable(typeof(Models.Version))]
[JsonSerializable(typeof(Models.RepositorySettings))]
diff --git a/src/Commands/GenerateCommitMessage.cs b/src/Commands/GenerateCommitMessage.cs
new file mode 100644
index 00000000..1bbf6cd8
--- /dev/null
+++ b/src/Commands/GenerateCommitMessage.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+
+namespace SourceGit.Commands
+{
+ ///
+ /// A C# version of https://github.com/anjerodev/commitollama
+ ///
+ public class GenerateCommitMessage
+ {
+ public class GetDiffContent : Command
+ {
+ public GetDiffContent(string repo, Models.DiffOption opt)
+ {
+ WorkingDirectory = repo;
+ Context = repo;
+ Args = $"diff --diff-algorithm=minimal {opt}";
+ }
+ }
+
+ public GenerateCommitMessage(string repo, List changes, CancellationToken cancelToken, Action onProgress)
+ {
+ _repo = repo;
+ _changes = changes;
+ _cancelToken = cancelToken;
+ _onProgress = onProgress;
+ }
+
+ public string Result()
+ {
+ try
+ {
+ var summaries = new List();
+ foreach (var change in _changes)
+ {
+ if (_cancelToken.IsCancellationRequested)
+ return "";
+
+ _onProgress?.Invoke($"Analyzing {change.Path}...");
+ var summary = GenerateChangeSummary(change);
+ summaries.Add(summary);
+ }
+
+ if (_cancelToken.IsCancellationRequested)
+ return "";
+
+ _onProgress?.Invoke($"Generating commit message...");
+ var builder = new StringBuilder();
+ builder.Append(GenerateSubject(string.Join("", summaries)));
+ builder.Append("\n");
+ foreach (var summary in summaries)
+ {
+ builder.Append("\n- ");
+ builder.Append(summary.Trim());
+ }
+
+ return builder.ToString();
+ }
+ catch (Exception e)
+ {
+ App.RaiseException(_repo, $"Failed to generate commit message: {e}");
+ return "";
+ }
+ }
+
+ private string GenerateChangeSummary(Models.Change change)
+ {
+ var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd();
+ var diff = rs.IsSuccess ? rs.StdOut : "unknown change";
+
+ var prompt = new StringBuilder();
+ prompt.AppendLine("You are an expert developer specialist in creating commits.");
+ prompt.AppendLine("Provide a super concise one sentence overall changes summary of the user `git diff` output following strictly the next rules:");
+ prompt.AppendLine("- Do not use any code snippets, imports, file routes or bullets points.");
+ prompt.AppendLine("- Do not mention the route of file that has been change.");
+ prompt.AppendLine("- Simply describe the MAIN GOAL of the changes.");
+ prompt.AppendLine("- Output directly the summary in plain text.`");
+
+ var rsp = Models.OpenAI.Chat(prompt.ToString(), $"Here is the `git diff` output: {diff}");
+ if (rsp != null && rsp.Choices.Count > 0)
+ return rsp.Choices[0].Message.Content;
+
+ return string.Empty;
+ }
+
+ private string GenerateSubject(string summary)
+ {
+ var prompt = new StringBuilder();
+ prompt.AppendLine("You are an expert developer specialist in creating commits messages.");
+ prompt.AppendLine("Your only goal is to retrieve a single commit message.");
+ prompt.AppendLine("Based on the provided user changes, combine them in ONE SINGLE commit message retrieving the global idea, following strictly the next rules:");
+ prompt.AppendLine("- Assign the commit {type} according to the next conditions:");
+ prompt.AppendLine(" feat: Only when adding a new feature.");
+ prompt.AppendLine(" fix: When fixing a bug.");
+ prompt.AppendLine(" docs: When updating documentation.");
+ prompt.AppendLine(" style: When changing elements styles or design and/or making changes to the code style (formatting, missing semicolons, etc.) without changing the code logic.");
+ prompt.AppendLine(" test: When adding or updating tests. ");
+ prompt.AppendLine(" chore: When making changes to the build process or auxiliary tools and libraries. ");
+ prompt.AppendLine(" revert: When undoing a previous commit.");
+ prompt.AppendLine(" refactor: When restructuring code without changing its external behavior, or is any of the other refactor types.");
+ prompt.AppendLine("- Do not add any issues numeration, explain your output nor introduce your answer.");
+ prompt.AppendLine("- Output directly only one commit message in plain text with the next format: {type}: {commit_message}.");
+ prompt.AppendLine("- Be as concise as possible, keep the message under 50 characters.");
+
+ var rsp = Models.OpenAI.Chat(prompt.ToString(), $"Here are the summaries changes: {summary}");
+ if (rsp != null && rsp.Choices.Count > 0)
+ return rsp.Choices[0].Message.Content;
+
+ return string.Empty;
+ }
+
+ private string _repo;
+ private List _changes;
+ private CancellationToken _cancelToken;
+ private Action _onProgress;
+ }
+}
diff --git a/src/Models/OpenAI.cs b/src/Models/OpenAI.cs
new file mode 100644
index 00000000..4acbe371
--- /dev/null
+++ b/src/Models/OpenAI.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace SourceGit.Models
+{
+ public class OpenAIChatMessage
+ {
+ [JsonPropertyName("role")]
+ public string Role
+ {
+ get;
+ set;
+ }
+
+ [JsonPropertyName("content")]
+ public string Content
+ {
+ get;
+ set;
+ }
+ }
+
+ public class OpenAIChatChoice
+ {
+ [JsonPropertyName("index")]
+ public int Index
+ {
+ get;
+ set;
+ }
+
+ [JsonPropertyName("message")]
+ public OpenAIChatMessage Message
+ {
+ get;
+ set;
+ }
+ }
+
+ public class OpenAIChatResponse
+ {
+ [JsonPropertyName("choices")]
+ public List Choices
+ {
+ get;
+ set;
+ } = [];
+ }
+
+ public class OpenAIChatRequest
+ {
+ [JsonPropertyName("model")]
+ public string Model
+ {
+ get;
+ set;
+ }
+
+ [JsonPropertyName("messages")]
+ public List Messages
+ {
+ get;
+ set;
+ } = [];
+
+ public void AddMessage(string role, string content)
+ {
+ Messages.Add(new OpenAIChatMessage { Role = role, Content = content });
+ }
+ }
+
+ public static class OpenAI
+ {
+ public static string Server
+ {
+ get;
+ set;
+ }
+
+ public static string ApiKey
+ {
+ get;
+ set;
+ }
+
+ public static string Model
+ {
+ get;
+ set;
+ }
+
+ public static bool IsValid
+ {
+ get => !string.IsNullOrEmpty(Server) && !string.IsNullOrEmpty(ApiKey) && !string.IsNullOrEmpty(Model);
+ }
+
+ public static OpenAIChatResponse Chat(string prompt, string question)
+ {
+ var chat = new OpenAIChatRequest() { Model = Model };
+ chat.AddMessage("system", prompt);
+ chat.AddMessage("user", question);
+
+ var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(60) };
+ client.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
+
+ var req = new StringContent(JsonSerializer.Serialize(chat, JsonCodeGen.Default.OpenAIChatRequest));
+ var task = client.PostAsync(Server, req);
+ task.Wait();
+
+ var rsp = task.Result;
+ if (!rsp.IsSuccessStatusCode)
+ throw new Exception($"AI service returns error code {rsp.StatusCode}");
+
+ var reader = rsp.Content.ReadAsStringAsync();
+ reader.Wait();
+
+ return JsonSerializer.Deserialize(reader.Result, JsonCodeGen.Default.OpenAIChatResponse);
+ }
+ }
+}
diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml
index 62c46ee6..0b32369c 100644
--- a/src/Resources/Icons.axaml
+++ b/src/Resources/Icons.axaml
@@ -1,4 +1,5 @@
+ M951 419a255 255 0 00-22-209 258 258 0 00-278-124A259 259 0 00213 178a255 255 0 00-171 124 258 258 0 0032 303 255 255 0 0022 210 258 258 0 00278 124A255 255 0 00566 1024a258 258 0 00246-179 256 256 0 00171-124 258 258 0 00-32-302zM566 957a191 191 0 01-123-44l6-3 204-118a34 34 0 0017-29v-287l86 50a3 3 0 012 2v238a192 192 0 01-192 192zM154 781a191 191 0 01-23-129l6 4 204 118a33 33 0 0033 0l249-144v99a3 3 0 01-1 3L416 851a192 192 0 01-262-70zM100 337a191 191 0 01101-84V495a33 33 0 0017 29l248 143-86 50a3 3 0 01-3 0l-206-119A192 192 0 01100 336zm708 164-249-145L645 307a3 3 0 013 0l206 119a192 192 0 01-29 346v-242a34 34 0 00-17-28zm86-129-6-4-204-119a33 33 0 00-33 0L401 394V294a3 3 0 011-3l206-119a192 192 0 01285 199zm-539 176-86-50a3 3 0 01-2-2V259a192 192 0 01315-147l-6 3-204 118a34 34 0 00-17 29zm47-101 111-64 111 64v128l-111 64-111-64z
M296 392h64v64h-64zM296 582v160h128V582h-64v-62h-64v62zm80 48v64h-32v-64h32zM360 328h64v64h-64zM296 264h64v64h-64zM360 456h64v64h-64zM360 200h64v64h-64zM855 289 639 73c-6-6-14-9-23-9H192c-18 0-32 14-32 32v832c0 18 14 32 32 32h640c18 0 32-14 32-32V311c0-9-3-17-9-23zM790 326H602V138L790 326zm2 562H232V136h64v64h64v-64h174v216c0 23 19 42 42 42h216v494z
M71 1024V0h661L953 219V1024H71zm808-731-220-219H145V951h735V293zM439 512h-220V219h220V512zm-74-219H292v146h74v-146zm0 512h74v73h-220v-73H292v-146H218V585h147v219zm294-366h74V512H512v-73h74v-146H512V219h147v219zm74 439H512V585h220v293zm-74-219h-74v146h74v-146z
M128 256h192a64 64 0 110 128H128a64 64 0 110-128zm576 192h192a64 64 0 010 128h-192a64 64 0 010-128zm-576 192h192a64 64 0 010 128H128a64 64 0 010-128zm576 0h192a64 64 0 010 128h-192a64 64 0 010-128zm0-384h192a64 64 0 010 128h-192a64 64 0 010-128zM128 448h192a64 64 0 110 128H128a64 64 0 110-128zm384-320a64 64 0 0164 64v640a64 64 0 01-128 0V192a64 64 0 0164-64z
diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml
index de801802..c56bb3b5 100644
--- a/src/Resources/Locales/en_US.axaml
+++ b/src/Resources/Locales/en_US.axaml
@@ -17,6 +17,8 @@
Optional. Default is the destination folder name.
Track Branch:
Tracking remote branch
+ OpenAI Assistant
+ Use OpenAI to generate commit message
Patch
Error
Raise errors and refuses to apply the patch
@@ -384,6 +386,10 @@
Last year
{0} years ago
Preference
+ OPEN AI
+ Server
+ API Key
+ Model
APPEARANCE
Default Font
Default Font Size
diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml
index 9ab76245..aed1c259 100644
--- a/src/Resources/Locales/zh_CN.axaml
+++ b/src/Resources/Locales/zh_CN.axaml
@@ -20,6 +20,8 @@
选填。默认使用目标文件夹名称。
跟踪分支
设置上游跟踪分支
+ OpenAI助手
+ 使用OpenAI助手生成提交信息
应用补丁(apply)
错误
输出错误,并终止应用补丁
diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml
index 90059b41..7816b604 100644
--- a/src/Resources/Locales/zh_TW.axaml
+++ b/src/Resources/Locales/zh_TW.axaml
@@ -20,6 +20,8 @@
選填。預設使用目標資料夾名稱。
追蹤分支
設定遠端追蹤分支
+ OpenAI 助手
+ 使用 OpenAI 產生提交消息
套用修補檔 (apply patch)
錯誤
輸出錯誤,並中止套用修補檔
diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml
index b5d4dddb..6de1a479 100644
--- a/src/Resources/Styles.axaml
+++ b/src/Resources/Styles.axaml
@@ -466,21 +466,6 @@
-
-
-
-
-