feature: supports to share issuetracker rules in .issuetracker file (#1567)

Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
leo
2025-07-18 13:09:34 +08:00
parent 69d6e5e29a
commit b467eddead
9 changed files with 142 additions and 1 deletions

View File

@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace SourceGit.Commands
{
public class SharedIssueTracker : Command
{
public SharedIssueTracker(string repo)
{
WorkingDirectory = repo;
Context = repo;
_file = $"{repo}/.issuetracker";
}
public async Task<List<Models.IssueTrackerRule>> ReadAllAsync()
{
if (!File.Exists(_file))
return [];
Args = $"config -f {_file.Quoted()} -l";
var output = await ReadToEndAsync().ConfigureAwait(false);
var rs = new List<Models.IssueTrackerRule>();
if (output.IsSuccess)
{
var lines = output.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var parts = line.Split('=', 2);
if (parts.Length < 2)
continue;
var key = parts[0];
var value = parts[1];
if (!key.StartsWith("issuetracker.", StringComparison.Ordinal))
continue;
if (key.EndsWith(".regex", StringComparison.Ordinal))
{
var prefixLen = "issuetracker.".Length;
var suffixLen = ".regex".Length;
var ruleName = key.Substring(prefixLen, key.Length - prefixLen - suffixLen);
FindOrAdd(rs, ruleName).RegexString = value;
}
else if (key.EndsWith(".url", StringComparison.Ordinal))
{
var prefixLen = "issuetracker.".Length;
var suffixLen = ".url".Length;
var ruleName = key.Substring(prefixLen, key.Length - prefixLen - suffixLen);
FindOrAdd(rs, ruleName).URLTemplate = value;
}
}
}
return rs;
}
public async Task<bool> AddAsync(Models.IssueTrackerRule rule)
{
Args = $"config -f {_file.Quoted()} issuetracker.{rule.Name.Quoted()}.regex {rule.RegexString.Quoted()}";
var succ = await ExecAsync().ConfigureAwait(false);
if (succ)
{
Args = $"config -f {_file.Quoted()} issuetracker.{rule.Name.Quoted()}.url {rule.URLTemplate.Quoted()}";
return await ExecAsync().ConfigureAwait(false);
}
return false;
}
public async Task<bool> RemoveAsync(Models.IssueTrackerRule rule)
{
Args = $"config -f {_file.Quoted()} --remove-section issuetracker.{rule.Name.Quoted()}";
return await ExecAsync().ConfigureAwait(false);
}
private Models.IssueTrackerRule FindOrAdd(List<Models.IssueTrackerRule> rules, string ruleName)
{
var rule = rules.Find(x => x.Name.Equals(ruleName, StringComparison.Ordinal));
if (rule != null)
return rule;
rule = new Models.IssueTrackerRule() { IsShared = true, Name = ruleName };
rules.Add(rule);
return rule;
}
private readonly string _file;
}
}

View File

@@ -6,6 +6,12 @@ namespace SourceGit.Models
{
public class IssueTrackerRule : ObservableObject
{
public bool IsShared
{
get => _isShared;
set => SetProperty(ref _isShared, value);
}
public string Name
{
get => _name;
@@ -70,6 +76,7 @@ namespace SourceGit.Models
}
}
private bool _isShared;
private string _name;
private string _regexString;
private string _urlTemplate;

View File

@@ -204,6 +204,7 @@
<x:String x:Key="Text.Configure.IssueTracker.NewRule" xml:space="preserve">New Rule</x:String>
<x:String x:Key="Text.Configure.IssueTracker.Regex" xml:space="preserve">Issue Regex Expression:</x:String>
<x:String x:Key="Text.Configure.IssueTracker.RuleName" xml:space="preserve">Rule Name:</x:String>
<x:String x:Key="Text.Configure.IssueTracker.Share" xml:space="preserve">Share this rule in .issuetracker file</x:String>
<x:String x:Key="Text.Configure.IssueTracker.URLTemplate" xml:space="preserve">Result URL:</x:String>
<x:String x:Key="Text.Configure.IssueTracker.URLTemplate.Tip" xml:space="preserve">Please use $1, $2 to access regex groups values.</x:String>
<x:String x:Key="Text.Configure.OpenAI" xml:space="preserve">AI</x:String>

View File

@@ -208,6 +208,7 @@
<x:String x:Key="Text.Configure.IssueTracker.NewRule" xml:space="preserve">新增自定义规则</x:String>
<x:String x:Key="Text.Configure.IssueTracker.Regex" xml:space="preserve">匹配ISSUE的正则表达式 </x:String>
<x:String x:Key="Text.Configure.IssueTracker.RuleName" xml:space="preserve">规则名 </x:String>
<x:String x:Key="Text.Configure.IssueTracker.Share" xml:space="preserve">写入 .issuetracker 文件共享此规则</x:String>
<x:String x:Key="Text.Configure.IssueTracker.URLTemplate" xml:space="preserve">为ISSUE生成的URL链接 </x:String>
<x:String x:Key="Text.Configure.IssueTracker.URLTemplate.Tip" xml:space="preserve">可在URL中使用$1$2等变量填入正则表达式匹配的内容</x:String>
<x:String x:Key="Text.Configure.OpenAI" xml:space="preserve">AI</x:String>

View File

@@ -208,6 +208,7 @@
<x:String x:Key="Text.Configure.IssueTracker.NewRule" xml:space="preserve">新增自訂規則</x:String>
<x:String x:Key="Text.Configure.IssueTracker.Regex" xml:space="preserve">符合 Issue 的正規表達式:</x:String>
<x:String x:Key="Text.Configure.IssueTracker.RuleName" xml:space="preserve">規則名稱:</x:String>
<x:String x:Key="Text.Configure.IssueTracker.Share" xml:space="preserve">寫入 .issuetracker 檔案以分享此規則</x:String>
<x:String x:Key="Text.Configure.IssueTracker.URLTemplate" xml:space="preserve">為 Issue 產生的網址連結:</x:String>
<x:String x:Key="Text.Configure.IssueTracker.URLTemplate.Tip" xml:space="preserve">可在網址中使用 $1、$2 等變數填入正規表達式相符的內容</x:String>
<x:String x:Key="Text.Configure.OpenAI" xml:space="preserve">AI</x:String>

View File

@@ -546,6 +546,13 @@ namespace SourceGit.ViewModels
_settings.LastCommitMessage = _workingCopy.CommitMessage;
var sharedIssueTrackers = new List<Models.IssueTrackerRule>();
foreach (var rule in _settings.IssueTrackerRules)
if (rule.IsShared)
sharedIssueTrackers.Add(rule);
_settings.IssueTrackerRules.RemoveAll(sharedIssueTrackers);
try
{
using var stream = File.Create(Path.Combine(_gitDir, "sourcegit.settings"));
@@ -704,6 +711,10 @@ namespace SourceGit.ViewModels
Task.Run(async () =>
{
var sharedIssueTrackers = await new Commands.SharedIssueTracker(_fullpath).ReadAllAsync().ConfigureAwait(false);
if (sharedIssueTrackers.Count > 0)
Dispatcher.UIThread.Post(() => _settings.IssueTrackerRules.InsertRange(0, sharedIssueTrackers));
var config = await new Commands.Config(_fullpath).ReadAllAsync().ConfigureAwait(false);
_hasAllowedSignersFile = config.TryGetValue("gpg.ssh.allowedSignersFile", out var allowedSignersFile) && !string.IsNullOrEmpty(allowedSignersFile);

View File

@@ -295,6 +295,17 @@ namespace SourceGit.ViewModels
SelectedIssueTrackerRule = null;
}
public async Task ChangeIssueTrackerShareModeAsync()
{
if (_selectedIssueTrackerRule is not { } rule)
return;
if (rule.IsShared)
await new Commands.SharedIssueTracker(_repo.FullPath).AddAsync(rule);
else
await new Commands.SharedIssueTracker(_repo.FullPath).RemoveAsync(rule);
}
public void AddNewCustomAction()
{
SelectedCustomAction = _repo.Settings.AddNewCustomAction();

View File

@@ -355,7 +355,7 @@
<ContentControl.DataTemplates>
<DataTemplate DataType="m:IssueTrackerRule">
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,32">
<TextBlock Grid.Row="0" Text="{DynamicResource Text.Configure.IssueTracker.RuleName}"/>
<TextBox Grid.Row="1" Margin="0,4,0,0" CornerRadius="3" Height="28" Text="{Binding Name, Mode=TwoWay}"/>
@@ -369,6 +369,12 @@
<TextBlock Grid.Row="4" Margin="0,12,0,0" Text="{DynamicResource Text.Configure.IssueTracker.URLTemplate}"/>
<TextBox Grid.Row="5" Margin="0,4,0,0" CornerRadius="3" Height="28" Text="{Binding URLTemplate, Mode=TwoWay}"/>
<TextBlock Grid.Row="6" Margin="0,2,0,0" Text="{DynamicResource Text.Configure.IssueTracker.URLTemplate.Tip}" Foreground="{DynamicResource Brush.FG2}"/>
<CheckBox Grid.Row="7" Grid.Column="1"
Margin="0,4,0,0"
Content="{DynamicResource Text.Configure.IssueTracker.Share}"
IsChecked="{Binding IsShared, Mode=TwoWay}"
Click="OnIssueTrackerIsSharedChanged"/>
</Grid>
</DataTemplate>
</ContentControl.DataTemplates>

View File

@@ -48,5 +48,14 @@ namespace SourceGit.Views
await dialog.ShowDialog(this);
e.Handled = true;
}
private async void OnIssueTrackerIsSharedChanged(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.RepositoryConfigure configure)
{
await configure.ChangeIssueTrackerShareModeAsync();
e.Handled = true;
}
}
}
}