Skip to content

Commit 33325df

Browse files
committed
feature: support .issuetracker file
Beforehand issue trackers could be defined in the repository configuration only. They were stored in the `.git` directory. As a consequence each SourceGit user had to configure issue trackers for himself. Now additional issue tracker rules are read from an existing `.issuetracker` file (#1424). An issue tracker configured by a user has a higher priority than an issue tracker read from `.issuetracker`. This allows to share issue trackers between team members. SourceGit does not (yet?) write into the `.issuetracker` file. But using `git config` allows the user to easily construct issue tracker rules for the `.issuetracker` file, where even the escaping of the backslash in '#(\d+)' is done correctly. A `.issuetracker` file was added to SourceGit. It was created by executing the following commands in a bash shell, where the current directory was the repository root: ```bash cat >.issuetracker <<EOF # Integration with Issue Tracker # # (note that '\' need to be escaped). EOF git config set --file .issuetracker issuetracker.GitHub.regex '#(\d+)' git config set --file .issuetracker issuetracker.GitHub.url \ 'https://github.com/sourcegit-scm/sourcegit/issues/$1' ```
1 parent 6ae8c7c commit 33325df

12 files changed

+130
-3
lines changed

.issuetracker

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Integration with Issue Tracker
2+
#
3+
# (note that '\' need to be escaped).
4+
5+
[issuetracker "GitHub"]
6+
regex = "#(\\d+)"
7+
url = https://github.com/sourcegit-scm/sourcegit/issues/$1

src/Commands/Config.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ public Config(string repository)
2020
}
2121
}
2222

23-
public async Task<Dictionary<string, string>> ReadAllAsync()
23+
public async Task<Dictionary<string, string>> ReadAllAsync(string file = null)
2424
{
25-
Args = "config -l";
25+
Args = string.IsNullOrEmpty(file) ? "config -l" : $"config -l -f {file}";
2626

2727
var output = await ReadToEndAsync().ConfigureAwait(false);
2828
var rs = new Dictionary<string, string>();

src/Models/IRepository.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ public interface IRepository
44
{
55
bool MayHaveSubmodules();
66

7+
void LoadSharedIssueTrackerRules();
8+
79
void RefreshBranches();
810
void RefreshWorktrees();
911
void RefreshTags();

src/Models/Watcher.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,12 @@ private void HandleWorkingCopyFileChanged(string name)
260260
return;
261261
}
262262

263+
if (name == ".issuetracker")
264+
{
265+
_repo.LoadSharedIssueTrackerRules();
266+
return;
267+
}
268+
263269
lock (_lockSubmodule)
264270
{
265271
foreach (var submodule in _submodules)

src/ViewModels/CommitDetail.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.ComponentModel;
34
using System.IO;
45
using System.Text.RegularExpressions;
56
using System.Threading;
@@ -145,10 +146,12 @@ public CommitDetail(Repository repo, bool rememberActivePageIndex)
145146
_repo = repo;
146147
_rememberActivePageIndex = rememberActivePageIndex;
147148
WebLinks = Models.CommitLink.Get(repo.Remotes);
149+
_repo.PropertyChanged += OnRepoPropertyChanged;
148150
}
149151

150152
public void Dispose()
151153
{
154+
_repo.PropertyChanged -= OnRepoPropertyChanged;
152155
_repo = null;
153156
_commit = null;
154157
_changes = null;
@@ -666,6 +669,12 @@ public ContextMenu CreateRevisionFileContextMenu(Models.Object file)
666669
return menu;
667670
}
668671

672+
private void OnRepoPropertyChanged(object sender, PropertyChangedEventArgs e)
673+
{
674+
if (e.PropertyName == nameof(Repository.SharedIssueTrackerRules))
675+
Refresh();
676+
}
677+
669678
private void Refresh()
670679
{
671680
_changes = null;
@@ -770,6 +779,12 @@ private void Refresh()
770779
rule.Matches(inlines, message);
771780
}
772781

782+
if (_repo.SharedIssueTrackerRules is { Count: > 0 } sharedRules)
783+
{
784+
foreach (var rule in sharedRules)
785+
rule.Matches(inlines, message);
786+
}
787+
773788
var urlMatches = REG_URL_FORMAT().Matches(message);
774789
foreach (Match match in urlMatches)
775790
{

src/ViewModels/InteractiveRebase.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ public AvaloniaList<Models.IssueTrackerRule> IssueTrackerRules
9595
get => _repo.Settings.IssueTrackerRules;
9696
}
9797

98+
public AvaloniaList<Models.IssueTrackerRule> SharedIssueTrackerRules
99+
{
100+
get => _repo.SharedIssueTrackerRules;
101+
}
102+
98103
public bool IsLoading
99104
{
100105
get => _isLoading;

src/ViewModels/Repository.cs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ public Models.RepositorySettings Settings
5151
get => _settings;
5252
}
5353

54+
public AvaloniaList<Models.IssueTrackerRule> SharedIssueTrackerRules
55+
{
56+
get => _sharedIssueTrackerRules;
57+
set => SetProperty(ref _sharedIssueTrackerRules, value);
58+
}
59+
5460
public Models.GitFlow GitFlow
5561
{
5662
get;
@@ -505,6 +511,8 @@ public void Open()
505511
_settings = new Models.RepositorySettings();
506512
}
507513

514+
LoadSharedIssueTrackerRules();
515+
508516
try
509517
{
510518
// For worktrees, we need to watch the $GIT_COMMON_DIR instead of the $GIT_DIR.
@@ -590,6 +598,51 @@ public void Close()
590598
_matchedFilesForSearching = null;
591599
}
592600

601+
public void LoadSharedIssueTrackerRules()
602+
{
603+
var issueTrackerFile = Path.Combine(_fullpath, ".issuetracker");
604+
if (File.Exists(issueTrackerFile))
605+
{
606+
Task.Run(async () =>
607+
{
608+
AvaloniaList<Models.IssueTrackerRule> rules;
609+
610+
try
611+
{
612+
var config = new Commands.Config(_fullpath);
613+
var trackers = await config.ReadAllAsync(".issuetracker").ConfigureAwait(false);
614+
615+
var map = new Dictionary<string, Models.IssueTrackerRule>();
616+
var lookup = map.GetAlternateLookup<ReadOnlySpan<char>>();
617+
618+
foreach (var entry in trackers)
619+
{
620+
var key = entry.Key.AsSpan();
621+
if (!key.StartsWith("issuetracker."))
622+
continue;
623+
624+
if (key.EndsWith(".regex"))
625+
GetOrCreateIssueTrackerRule(lookup, key[13 .. ^6]).RegexString = entry.Value;
626+
else if (key.EndsWith(".url"))
627+
GetOrCreateIssueTrackerRule(lookup, key[13 .. ^4]).URLTemplate = entry.Value;
628+
}
629+
630+
rules = new AvaloniaList<Models.IssueTrackerRule>(map.Values);
631+
}
632+
catch
633+
{
634+
rules = [];
635+
}
636+
637+
Dispatcher.UIThread.Post(() => SharedIssueTrackerRules = rules);
638+
});
639+
}
640+
else
641+
{
642+
SharedIssueTrackerRules = [];
643+
}
644+
}
645+
593646
public bool CanCreatePopup()
594647
{
595648
var page = GetOwnerPage();
@@ -2803,6 +2856,17 @@ public ContextMenu CreateContextMenuForWorktree(Models.Worktree worktree)
28032856
return menu;
28042857
}
28052858

2859+
private static Models.IssueTrackerRule GetOrCreateIssueTrackerRule(
2860+
Dictionary<string, Models.IssueTrackerRule>.AlternateLookup<ReadOnlySpan<char>> lookup,
2861+
ReadOnlySpan<char> name)
2862+
{
2863+
if (lookup.TryGetValue(name, out var existingRule))
2864+
return existingRule;
2865+
2866+
var newRule = new Models.IssueTrackerRule { Name = name.ToString() };
2867+
return lookup[name] = newRule;
2868+
}
2869+
28062870
private LauncherPage GetOwnerPage()
28072871
{
28082872
var launcher = App.GetLauncher();
@@ -3101,6 +3165,7 @@ private async void AutoFetchImpl(object sender)
31013165
private string _fullpath = string.Empty;
31023166
private string _gitDir = string.Empty;
31033167
private Models.RepositorySettings _settings = null;
3168+
private AvaloniaList<Models.IssueTrackerRule> _sharedIssueTrackerRules = null;
31043169
private Models.FilterMode _historiesFilterMode = Models.FilterMode.None;
31053170
private bool _hasAllowedSignersFile = false;
31063171

src/Views/CommitSubjectPresenter.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ public AvaloniaList<Models.IssueTrackerRule> IssueTrackerRules
9393
set => SetValue(IssueTrackerRulesProperty, value);
9494
}
9595

96+
public static readonly StyledProperty<AvaloniaList<Models.IssueTrackerRule>> SharedIssueTrackerRulesProperty =
97+
AvaloniaProperty.Register<CommitSubjectPresenter, AvaloniaList<Models.IssueTrackerRule>>(nameof(SharedIssueTrackerRules));
98+
99+
public AvaloniaList<Models.IssueTrackerRule> SharedIssueTrackerRules
100+
{
101+
get => GetValue(SharedIssueTrackerRulesProperty);
102+
set => SetValue(SharedIssueTrackerRulesProperty, value);
103+
}
104+
96105
public override void Render(DrawingContext context)
97106
{
98107
if (_needRebuildInlines)
@@ -138,7 +147,9 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
138147
{
139148
base.OnPropertyChanged(change);
140149

141-
if (change.Property == SubjectProperty || change.Property == IssueTrackerRulesProperty)
150+
if (change.Property == SubjectProperty ||
151+
change.Property == IssueTrackerRulesProperty ||
152+
change.Property == SharedIssueTrackerRulesProperty)
142153
{
143154
_elements.Clear();
144155
ClearHoveredIssueLink();
@@ -155,6 +166,10 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
155166
foreach (var rule in rules)
156167
rule.Matches(_elements, subject);
157168

169+
var sharedRules = SharedIssueTrackerRules ?? [];
170+
foreach (var rule in sharedRules)
171+
rule.Matches(_elements, subject);
172+
158173
var keywordMatch = REG_KEYWORD_FORMAT1().Match(subject);
159174
if (!keywordMatch.Success)
160175
keywordMatch = REG_KEYWORD_FORMAT2().Match(subject);

src/Views/Histories.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
LinkForeground="{DynamicResource Brush.Link}"
143143
Subject="{Binding Subject}"
144144
IssueTrackerRules="{Binding $parent[v:Histories].IssueTrackerRules}"
145+
SharedIssueTrackerRules="{Binding $parent[v:Histories].SharedIssueTrackerRules}"
145146
FontWeight="{Binding FontWeight}"
146147
Opacity="{Binding Opacity}"/>
147148
</Grid>

src/Views/Histories.axaml.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@ public AvaloniaList<Models.IssueTrackerRule> IssueTrackerRules
103103
set => SetValue(IssueTrackerRulesProperty, value);
104104
}
105105

106+
public static readonly StyledProperty<AvaloniaList<Models.IssueTrackerRule>> SharedIssueTrackerRulesProperty =
107+
AvaloniaProperty.Register<Histories, AvaloniaList<Models.IssueTrackerRule>>(nameof(SharedIssueTrackerRules));
108+
109+
public AvaloniaList<Models.IssueTrackerRule> SharedIssueTrackerRules
110+
{
111+
get => GetValue(SharedIssueTrackerRulesProperty);
112+
set => SetValue(SharedIssueTrackerRulesProperty, value);
113+
}
114+
106115
public static readonly StyledProperty<bool> OnlyHighlightCurrentBranchProperty =
107116
AvaloniaProperty.Register<Histories, bool>(nameof(OnlyHighlightCurrentBranch), true);
108117

0 commit comments

Comments
 (0)