Skip to content

Commit 94f75d7

Browse files
committed
Merge branch 'release/v8.30'
2 parents 8630096 + 8393d7e commit 94f75d7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+2405
-978
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ Opensource Git GUI client.
3434
* GitFlow
3535
* Git LFS
3636
* Issue Link
37+
* Workspace
38+
* Using AI to generate commit message (C# port of [anjerodev/commitollama](https://github.com/anjerodev/commitollama))
3739

3840
> [!WARNING]
3941
> **Linux** only tested on **Debian 12** on both **X11** & **Wayland**.
@@ -83,6 +85,20 @@ For **Linux** users:
8385

8486
* `xdg-open` must be installed to support open native file manager.
8587
* Make sure [git-credential-manager](https://github.com/git-ecosystem/git-credential-manager/releases) is installed on your linux.
88+
* Maybe you need to set environment variable `AVALONIA_SCREEN_SCALE_FACTORS`. See https://github.com/AvaloniaUI/Avalonia/wiki/Configuring-X11-per-monitor-DPI.
89+
90+
## OpenAI
91+
92+
This software supports using OpenAI or other AI service that has an OpenAI comaptible HTTP API to generate commit message. You need configurate the service in `Preference` window.
93+
94+
For `OpenAI`:
95+
96+
* `Server` must be `https://api.openai.com/v1/chat/completions`
97+
98+
For other AI service:
99+
100+
* The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1/chat/completions`
101+
* The `API Key` is optional that depends on the service
86102

87103
## External Tools
88104

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
8.29
1+
8.30

src/App.JsonCodeGen.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public override void Write(Utf8JsonWriter writer, GridLength value, JsonSerializ
4646
[JsonSerializable(typeof(Models.ExternalToolPaths))]
4747
[JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))]
4848
[JsonSerializable(typeof(Models.JetBrainsState))]
49+
[JsonSerializable(typeof(Models.OpenAIChatRequest))]
50+
[JsonSerializable(typeof(Models.OpenAIChatResponse))]
4951
[JsonSerializable(typeof(Models.ThemeOverrides))]
5052
[JsonSerializable(typeof(Models.Version))]
5153
[JsonSerializable(typeof(Models.RepositorySettings))]

src/Commands/GenerateCommitMessage.cs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using System.Threading;
5+
6+
namespace SourceGit.Commands
7+
{
8+
/// <summary>
9+
/// A C# version of https://github.com/anjerodev/commitollama
10+
/// </summary>
11+
public class GenerateCommitMessage
12+
{
13+
public class GetDiffContent : Command
14+
{
15+
public GetDiffContent(string repo, Models.DiffOption opt)
16+
{
17+
WorkingDirectory = repo;
18+
Context = repo;
19+
Args = $"diff --diff-algorithm=minimal {opt}";
20+
}
21+
}
22+
23+
public GenerateCommitMessage(string repo, List<Models.Change> changes, CancellationToken cancelToken, Action<string> onProgress)
24+
{
25+
_repo = repo;
26+
_changes = changes;
27+
_cancelToken = cancelToken;
28+
_onProgress = onProgress;
29+
}
30+
31+
public string Result()
32+
{
33+
try
34+
{
35+
var summaries = new List<string>();
36+
foreach (var change in _changes)
37+
{
38+
if (_cancelToken.IsCancellationRequested)
39+
return "";
40+
41+
_onProgress?.Invoke($"Analyzing {change.Path}...");
42+
var summary = GenerateChangeSummary(change);
43+
summaries.Add(summary);
44+
}
45+
46+
if (_cancelToken.IsCancellationRequested)
47+
return "";
48+
49+
_onProgress?.Invoke($"Generating commit message...");
50+
var builder = new StringBuilder();
51+
builder.Append(GenerateSubject(string.Join("", summaries)));
52+
builder.Append("\n");
53+
foreach (var summary in summaries)
54+
{
55+
builder.Append("\n- ");
56+
builder.Append(summary.Trim());
57+
}
58+
59+
return builder.ToString();
60+
}
61+
catch (Exception e)
62+
{
63+
App.RaiseException(_repo, $"Failed to generate commit message: {e}");
64+
return "";
65+
}
66+
}
67+
68+
private string GenerateChangeSummary(Models.Change change)
69+
{
70+
var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd();
71+
var diff = rs.IsSuccess ? rs.StdOut : "unknown change";
72+
73+
var prompt = new StringBuilder();
74+
prompt.AppendLine("You are an expert developer specialist in creating commits.");
75+
prompt.AppendLine("Provide a super concise one sentence overall changes summary of the user `git diff` output following strictly the next rules:");
76+
prompt.AppendLine("- Do not use any code snippets, imports, file routes or bullets points.");
77+
prompt.AppendLine("- Do not mention the route of file that has been change.");
78+
prompt.AppendLine("- Simply describe the MAIN GOAL of the changes.");
79+
prompt.AppendLine("- Output directly the summary in plain text.`");
80+
81+
var rsp = Models.OpenAI.Chat(prompt.ToString(), $"Here is the `git diff` output: {diff}", _cancelToken);
82+
if (rsp != null && rsp.Choices.Count > 0)
83+
return rsp.Choices[0].Message.Content;
84+
85+
return string.Empty;
86+
}
87+
88+
private string GenerateSubject(string summary)
89+
{
90+
var prompt = new StringBuilder();
91+
prompt.AppendLine("You are an expert developer specialist in creating commits messages.");
92+
prompt.AppendLine("Your only goal is to retrieve a single commit message.");
93+
prompt.AppendLine("Based on the provided user changes, combine them in ONE SINGLE commit message retrieving the global idea, following strictly the next rules:");
94+
prompt.AppendLine("- Assign the commit {type} according to the next conditions:");
95+
prompt.AppendLine(" feat: Only when adding a new feature.");
96+
prompt.AppendLine(" fix: When fixing a bug.");
97+
prompt.AppendLine(" docs: When updating documentation.");
98+
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.");
99+
prompt.AppendLine(" test: When adding or updating tests. ");
100+
prompt.AppendLine(" chore: When making changes to the build process or auxiliary tools and libraries. ");
101+
prompt.AppendLine(" revert: When undoing a previous commit.");
102+
prompt.AppendLine(" refactor: When restructuring code without changing its external behavior, or is any of the other refactor types.");
103+
prompt.AppendLine("- Do not add any issues numeration, explain your output nor introduce your answer.");
104+
prompt.AppendLine("- Output directly only one commit message in plain text with the next format: {type}: {commit_message}.");
105+
prompt.AppendLine("- Be as concise as possible, keep the message under 50 characters.");
106+
107+
var rsp = Models.OpenAI.Chat(prompt.ToString(), $"Here are the summaries changes: {summary}", _cancelToken);
108+
if (rsp != null && rsp.Choices.Count > 0)
109+
return rsp.Choices[0].Message.Content;
110+
111+
return string.Empty;
112+
}
113+
114+
private string _repo;
115+
private List<Models.Change> _changes;
116+
private CancellationToken _cancelToken;
117+
private Action<string> _onProgress;
118+
}
119+
}

src/Converters/BookmarkConverters.cs

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/Converters/IntConverters.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using Avalonia;
2+
using Avalonia.Controls;
23
using Avalonia.Data.Converters;
4+
using Avalonia.Media;
35

46
namespace SourceGit.Converters
57
{
@@ -28,5 +30,14 @@ public static class IntConverters
2830

2931
public static readonly FuncValueConverter<int, Thickness> ToTreeMargin =
3032
new FuncValueConverter<int, Thickness>(v => new Thickness(v * 16, 0, 0, 0));
33+
34+
public static readonly FuncValueConverter<int, IBrush> ToBookmarkBrush =
35+
new FuncValueConverter<int, IBrush>(bookmark =>
36+
{
37+
if (bookmark == 0)
38+
return Application.Current?.FindResource("Brush.FG1") as IBrush;
39+
else
40+
return Models.Bookmarks.Brushes[bookmark];
41+
});
3142
}
3243
}

src/Models/OpenAI.cs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Net.Http;
4+
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
using System.Threading;
7+
8+
namespace SourceGit.Models
9+
{
10+
public class OpenAIChatMessage
11+
{
12+
[JsonPropertyName("role")]
13+
public string Role
14+
{
15+
get;
16+
set;
17+
}
18+
19+
[JsonPropertyName("content")]
20+
public string Content
21+
{
22+
get;
23+
set;
24+
}
25+
}
26+
27+
public class OpenAIChatChoice
28+
{
29+
[JsonPropertyName("index")]
30+
public int Index
31+
{
32+
get;
33+
set;
34+
}
35+
36+
[JsonPropertyName("message")]
37+
public OpenAIChatMessage Message
38+
{
39+
get;
40+
set;
41+
}
42+
}
43+
44+
public class OpenAIChatResponse
45+
{
46+
[JsonPropertyName("choices")]
47+
public List<OpenAIChatChoice> Choices
48+
{
49+
get;
50+
set;
51+
} = [];
52+
}
53+
54+
public class OpenAIChatRequest
55+
{
56+
[JsonPropertyName("model")]
57+
public string Model
58+
{
59+
get;
60+
set;
61+
}
62+
63+
[JsonPropertyName("messages")]
64+
public List<OpenAIChatMessage> Messages
65+
{
66+
get;
67+
set;
68+
} = [];
69+
70+
public void AddMessage(string role, string content)
71+
{
72+
Messages.Add(new OpenAIChatMessage { Role = role, Content = content });
73+
}
74+
}
75+
76+
public static class OpenAI
77+
{
78+
public static string Server
79+
{
80+
get;
81+
set;
82+
}
83+
84+
public static string ApiKey
85+
{
86+
get;
87+
set;
88+
}
89+
90+
public static string Model
91+
{
92+
get;
93+
set;
94+
}
95+
96+
public static bool IsValid
97+
{
98+
get => !string.IsNullOrEmpty(Server) && !string.IsNullOrEmpty(Model);
99+
}
100+
101+
public static OpenAIChatResponse Chat(string prompt, string question, CancellationToken cancellation)
102+
{
103+
var chat = new OpenAIChatRequest() { Model = Model };
104+
chat.AddMessage("system", prompt);
105+
chat.AddMessage("user", question);
106+
107+
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(60) };
108+
client.DefaultRequestHeaders.Add("Content-Type", "application/json");
109+
if (!string.IsNullOrEmpty(ApiKey))
110+
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
111+
112+
var req = new StringContent(JsonSerializer.Serialize(chat, JsonCodeGen.Default.OpenAIChatRequest));
113+
try
114+
{
115+
var task = client.PostAsync(Server, req, cancellation);
116+
task.Wait();
117+
118+
var rsp = task.Result;
119+
if (!rsp.IsSuccessStatusCode)
120+
throw new Exception($"AI service returns error code {rsp.StatusCode}");
121+
122+
var reader = rsp.Content.ReadAsStringAsync(cancellation);
123+
reader.Wait();
124+
125+
return JsonSerializer.Deserialize(reader.Result, JsonCodeGen.Default.OpenAIChatResponse);
126+
}
127+
catch
128+
{
129+
if (cancellation.IsCancellationRequested)
130+
return null;
131+
132+
throw;
133+
}
134+
}
135+
}
136+
}

src/Models/RevisionFile.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ public class RevisionBinaryFile
1010
public class RevisionImageFile
1111
{
1212
public Bitmap Image { get; set; } = null;
13+
public long FileSize { get; set; } = 0;
14+
public string ImageType { get; set; } = string.Empty;
15+
public string ImageSize => Image != null ? $"{Image.PixelSize.Width} x {Image.PixelSize.Height}" : "0 x 0";
1316
}
1417

1518
public class RevisionTextFile

src/Models/Shell.cs

Lines changed: 0 additions & 10 deletions
This file was deleted.

0 commit comments

Comments
 (0)