fix: modified/renamed issue tracker rule can not be saved (#1768)

This commit also change the way to save modified issue tracker - It will be saved when closing the `Repository Configuration` dialog.

Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
leo
2025-08-27 15:33:03 +08:00
parent aa26885514
commit ecfe9145a3
6 changed files with 131 additions and 93 deletions

View File

@@ -7,20 +7,21 @@ namespace SourceGit.Commands
{
public class IssueTracker : Command
{
public IssueTracker(string repo, string storage)
public IssueTracker(string repo, bool isShared)
{
WorkingDirectory = repo;
Context = repo;
if (string.IsNullOrEmpty(storage))
if (isShared)
{
_isStorageFileExists = true;
_baseArg = "config --local";
var storage = $"{repo}/.issuetracker";
_isStorageFileExists = File.Exists(storage);
_baseArg = $"config -f {storage.Quoted()}";
}
else
{
_isStorageFileExists = File.Exists(storage);
_baseArg = $"config -f {storage.Quoted()}";
_isStorageFileExists = true;
_baseArg = "config --local";
}
}
@@ -79,12 +80,24 @@ namespace SourceGit.Commands
return false;
}
public async Task<bool> RemoveAsync(Models.IssueTracker rule)
public async Task<bool> UpdateRegexAsync(Models.IssueTracker rule)
{
Args = $"{_baseArg} issuetracker.{rule.Name.Quoted()}.regex {rule.RegexString.Quoted()}";
return await ExecAsync().ConfigureAwait(false);
}
public async Task<bool> UpdateURLTemplateAsync(Models.IssueTracker rule)
{
Args = $"{_baseArg} issuetracker.{rule.Name.Quoted()}.url {rule.URLTemplate.Quoted()}";
return await ExecAsync().ConfigureAwait(false);
}
public async Task<bool> RemoveAsync(string name)
{
if (!_isStorageFileExists)
return true;
Args = $"{_baseArg} --remove-section issuetracker.{rule.Name.Quoted()}";
Args = $"{_baseArg} --remove-section issuetracker.{name.Quoted()}";
return await ExecAsync().ConfigureAwait(false);
}

View File

@@ -26,12 +26,11 @@ namespace SourceGit.Models
{
try
{
_regex = null;
_regex = new Regex(_regexString, RegexOptions.Multiline);
}
catch
{
// Ignore errors.
_regex = null;
}
}

View File

@@ -740,39 +740,6 @@ namespace SourceGit.ViewModels
return log;
}
public async Task<Models.IssueTracker> AddIssueTrackerAsync(string name, string regex, string url)
{
var rule = new Models.IssueTracker()
{
IsShared = false,
Name = name,
RegexString = regex,
URLTemplate = url,
};
var succ = await CreateIssueTrackerCommand(false).AddAsync(rule);
if (succ)
{
IssueTrackers.Add(rule);
return rule;
}
return null;
}
public async Task RemoveIssueTrackerAsync(Models.IssueTracker rule)
{
var succ = await CreateIssueTrackerCommand(rule.IsShared).RemoveAsync(rule);
if (succ)
IssueTrackers.Remove(rule);
}
public async Task ChangeIssueTrackerShareModeAsync(Models.IssueTracker rule)
{
await CreateIssueTrackerCommand(!rule.IsShared).RemoveAsync(rule);
await CreateIssueTrackerCommand(rule.IsShared).AddAsync(rule);
}
public void RefreshAll()
{
RefreshCommits();
@@ -786,8 +753,8 @@ namespace SourceGit.ViewModels
Task.Run(async () =>
{
var issuetrackers = new List<Models.IssueTracker>();
await CreateIssueTrackerCommand(true).ReadAllAsync(issuetrackers, true).ConfigureAwait(false);
await CreateIssueTrackerCommand(false).ReadAllAsync(issuetrackers, false).ConfigureAwait(false);
await new Commands.IssueTracker(FullPath, true).ReadAllAsync(issuetrackers, true).ConfigureAwait(false);
await new Commands.IssueTracker(FullPath, false).ReadAllAsync(issuetrackers, false).ConfigureAwait(false);
Dispatcher.UIThread.Post(() =>
{
IssueTrackers.Clear();
@@ -1743,11 +1710,6 @@ namespace SourceGit.ViewModels
return null;
}
private Commands.IssueTracker CreateIssueTrackerCommand(bool shared)
{
return new Commands.IssueTracker(FullPath, shared ? $"{FullPath}/.issuetracker" : null);
}
private BranchTreeNode.Builder BuildBranchTree(List<Models.Branch> branches, List<Models.Remote> remotes)
{
var builder = new BranchTreeNode.Builder(_settings.LocalBranchSortMode, _settings.RemoteBranchSortMode);

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Collections;
@@ -114,8 +115,8 @@ namespace SourceGit.ViewModels
public AvaloniaList<Models.IssueTracker> IssueTrackers
{
get => _repo.IssueTrackers;
}
get;
} = [];
public Models.IssueTracker SelectedIssueTracker
{
@@ -176,6 +177,17 @@ namespace SourceGit.ViewModels
HttpProxy = proxy;
if (_cached.TryGetValue("fetch.prune", out var prune))
EnablePruneOnFetch = (prune == "true");
foreach (var rule in _repo.IssueTrackers)
{
IssueTrackers.Add(new()
{
IsShared = rule.IsShared,
Name = rule.Name,
RegexString = rule.RegexString,
URLTemplate = rule.URLTemplate,
});
}
}
public void ClearHttpProxy()
@@ -208,25 +220,28 @@ namespace SourceGit.ViewModels
return outs;
}
public async Task AddIssueTrackerAsync(string name, string regex, string url)
public void AddIssueTracker(string name, string regex, string url)
{
SelectedIssueTracker = await _repo.AddIssueTrackerAsync(name, regex, url);
var rule = new Models.IssueTracker()
{
IsShared = false,
Name = name,
RegexString = regex,
URLTemplate = url,
};
IssueTrackers.Add(rule);
SelectedIssueTracker = rule;
}
public async Task RemoveIssueTrackerAsync()
public void RemoveIssueTracker()
{
if (_selectedIssueTracker is { } rule)
await _repo.RemoveIssueTrackerAsync(rule);
IssueTrackers.Remove(rule);
SelectedIssueTracker = null;
}
public async Task ChangeIssueTrackerShareModeAsync()
{
if (_selectedIssueTracker is { } rule)
await _repo.ChangeIssueTrackerShareModeAsync(rule);
}
public void AddNewCustomAction()
{
SelectedCustomAction = _repo.Settings.AddNewCustomAction();
@@ -259,6 +274,8 @@ namespace SourceGit.ViewModels
await SetIfChangedAsync("user.signingkey", GPGUserSigningKey, "");
await SetIfChangedAsync("http.proxy", HttpProxy, "");
await SetIfChangedAsync("fetch.prune", EnablePruneOnFetch ? "true" : "false", "false");
await ApplyIssueTrackerChangesAsync();
}
private async Task SetIfChangedAsync(string key, string value, string defValue)
@@ -267,6 +284,62 @@ namespace SourceGit.ViewModels
await new Commands.Config(_repo.FullPath).SetAsync(key, value);
}
private async Task ApplyIssueTrackerChangesAsync()
{
var changed = false;
var oldRules = new Dictionary<string, Models.IssueTracker>();
foreach (var rule in _repo.IssueTrackers)
oldRules.Add(rule.Name, rule);
foreach (var rule in IssueTrackers)
{
if (oldRules.TryGetValue(rule.Name, out var old))
{
if (old.IsShared != rule.IsShared)
{
changed = true;
await new Commands.IssueTracker(_repo.FullPath, old.IsShared).RemoveAsync(old.Name);
await new Commands.IssueTracker(_repo.FullPath, rule.IsShared).AddAsync(rule);
}
else
{
if (!old.RegexString.Equals(rule.RegexString, StringComparison.Ordinal))
{
changed = true;
await new Commands.IssueTracker(_repo.FullPath, old.IsShared).UpdateRegexAsync(rule);
}
if (!old.URLTemplate.Equals(rule.URLTemplate, StringComparison.Ordinal))
{
changed = true;
await new Commands.IssueTracker(_repo.FullPath, old.IsShared).UpdateURLTemplateAsync(rule);
}
}
oldRules.Remove(rule.Name);
}
else
{
changed = true;
await new Commands.IssueTracker(_repo.FullPath, rule.IsShared).AddAsync(rule);
}
}
if (oldRules.Count > 0)
{
changed = true;
foreach (var kv in oldRules)
await new Commands.IssueTracker(_repo.FullPath, kv.Value.IsShared).RemoveAsync(kv.Key);
}
if (changed)
{
_repo.IssueTrackers.Clear();
_repo.IssueTrackers.AddRange(IssueTrackers);
}
}
private readonly Repository _repo = null;
private readonly Dictionary<string, string> _cached = null;
private string _httpProxy;

View File

@@ -374,8 +374,7 @@
<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"/>
IsChecked="{Binding IsShared, Mode=TwoWay}"/>
</Grid>
</DataTemplate>
</ContentControl.DataTemplates>

View File

@@ -50,15 +50,15 @@ namespace SourceGit.Views
e.Handled = true;
}
private async void OnNewCustomIssueTracker(object sender, RoutedEventArgs e)
private void OnNewCustomIssueTracker(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.RepositoryConfigure vm)
await vm.AddIssueTrackerAsync("New Issue Tracker", @"#(\d+)", "https://xxx/$1");
vm.AddIssueTracker("New Issue Tracker", @"#(\d+)", "https://xxx/$1");
e.Handled = true;
}
private async void OnAddGitHubIssueTracker(object sender, RoutedEventArgs e)
private void OnAddGitHubIssueTracker(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.RepositoryConfigure vm)
{
@@ -73,17 +73,17 @@ namespace SourceGit.Views
}
}
await vm.AddIssueTrackerAsync("GitHub Issue", @"#(\d+)", link);
vm.AddIssueTracker("GitHub Issue", @"#(\d+)", link);
}
e.Handled = true;
}
private async void OnAddJiraIssueTracker(object sender, RoutedEventArgs e)
private void OnAddJiraIssueTracker(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.RepositoryConfigure vm)
{
await vm.AddIssueTrackerAsync(
vm.AddIssueTracker(
"Jira Tracker",
@"PROJ-(\d+)",
"https://jira.yourcompany.com/browse/PROJ-$1");
@@ -92,11 +92,11 @@ namespace SourceGit.Views
e.Handled = true;
}
private async void OnAddAzureWorkItemTracker(object sender, RoutedEventArgs e)
private void OnAddAzureWorkItemTracker(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.RepositoryConfigure vm)
{
await vm.AddIssueTrackerAsync(
vm.AddIssueTracker(
"Azure DevOps Tracker",
@"#(\d+)",
"https://dev.azure.com/yourcompany/workspace/_workitems/edit/$1");
@@ -105,7 +105,7 @@ namespace SourceGit.Views
e.Handled = true;
}
private async void OnAddGitLabIssueTracker(object sender, RoutedEventArgs e)
private void OnAddGitLabIssueTracker(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.RepositoryConfigure vm)
{
@@ -117,13 +117,13 @@ namespace SourceGit.Views
break;
}
await vm.AddIssueTrackerAsync("GitLab Issue", @"#(\d+)", link);
vm.AddIssueTracker("GitLab Issue", @"#(\d+)", link);
}
e.Handled = true;
}
private async void OnAddGitLabMergeRequestTracker(object sender, RoutedEventArgs e)
private void OnAddGitLabMergeRequestTracker(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.RepositoryConfigure vm)
{
@@ -135,13 +135,13 @@ namespace SourceGit.Views
break;
}
await vm.AddIssueTrackerAsync("GitLab MR", @"!(\d+)", link);
vm.AddIssueTracker("GitLab MR", @"!(\d+)", link);
}
e.Handled = true;
}
private async void OnAddGiteeIssueTracker(object sender, RoutedEventArgs e)
private void OnAddGiteeIssueTracker(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.RepositoryConfigure vm)
{
@@ -156,13 +156,13 @@ namespace SourceGit.Views
}
}
await vm.AddIssueTrackerAsync("Gitee Issue", @"#([0-9A-Z]{6,10})", link);
vm.AddIssueTracker("Gitee Issue", @"#([0-9A-Z]{6,10})", link);
}
e.Handled = true;
}
private async void OnAddGiteePullRequestTracker(object sender, RoutedEventArgs e)
private void OnAddGiteePullRequestTracker(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.RepositoryConfigure vm)
{
@@ -177,17 +177,17 @@ namespace SourceGit.Views
}
}
await vm.AddIssueTrackerAsync("Gitee Pull Request", @"!(\d+)", link);
vm.AddIssueTracker("Gitee Pull Request", @"!(\d+)", link);
}
e.Handled = true;
}
private async void OnAddGerritChangeIdTracker(object sender, RoutedEventArgs e)
private void OnAddGerritChangeIdTracker(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.RepositoryConfigure vm)
{
await vm.AddIssueTrackerAsync(
vm.AddIssueTracker(
"Gerrit Change-Id",
@"(I[A-Za-z0-9]{40})",
"https://gerrit.yourcompany.com/q/$1");
@@ -196,18 +196,10 @@ namespace SourceGit.Views
e.Handled = true;
}
private async void OnRemoveIssueTracker(object sender, RoutedEventArgs e)
private void OnRemoveIssueTracker(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.RepositoryConfigure vm)
await vm.RemoveIssueTrackerAsync();
e.Handled = true;
}
private async void OnIssueTrackerIsSharedChanged(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.RepositoryConfigure vm)
await vm.ChangeIssueTrackerShareModeAsync();
vm.RemoveIssueTracker();
e.Handled = true;
}