feature: supports issue tracker in commit message (#315)

This commit is contained in:
leo 2024-08-05 17:34:49 +08:00
parent fa1f4155da
commit f754b2c63a
No known key found for this signature in database
20 changed files with 563 additions and 85 deletions

View file

@ -64,5 +64,6 @@ namespace SourceGit
[JsonSerializable(typeof(Models.Version))]
[JsonSerializable(typeof(Models.RepositorySettings))]
[JsonSerializable(typeof(ViewModels.Preference))]
[JsonSerializable(typeof(ViewModels.IssueTrackerRuleSetting))]
internal partial class JsonCodeGen : JsonSerializerContext { }
}

View file

@ -52,6 +52,7 @@
<StreamGeometry x:Key="Icons.Info">M512 0C229 0 0 229 0 512s229 512 512 512 512-229 512-512S795 0 512 0zM512 928c-230 0-416-186-416-416S282 96 512 96s416 186 416 416S742 928 512 928zM538 343c47 0 83-38 83-78 0-32-21-61-62-61-55 0-82 45-82 77C475 320 498 343 538 343zM533 729c-8 0-11-10-3-40l43-166c16-61 11-100-22-100-39 0-131 40-211 108l16 27c25-17 68-35 78-35 8 0 7 10 0 36l-38 158c-23 89 1 110 34 110 33 0 118-30 196-110l-19-25C575 717 543 729 533 729z</StreamGeometry>
<StreamGeometry x:Key="Icons.Init">M412 66C326 132 271 233 271 347c0 17 1 34 4 50-41-48-98-79-162-83a444 444 0 00-46 196c0 207 142 382 337 439h2c19 0 34 15 34 33 0 11-6 21-14 26l1 14C183 973 0 763 0 511 0 272 166 70 393 7A35 35 0 01414 0c19 0 34 15 34 33a33 33 0 01-36 33zm200 893c86-66 141-168 141-282 0-17-1-34-4-50 41 48 98 79 162 83a444 444 0 0046-196c0-207-142-382-337-439h-2a33 33 0 01-34-33c0-11 6-21 14-26L596 0C841 51 1024 261 1024 513c0 239-166 441-393 504A35 35 0 01610 1024a33 33 0 01-34-33 33 33 0 0136-33zM512 704a192 192 0 110-384 192 192 0 010 384z</StreamGeometry>
<StreamGeometry x:Key="Icons.InteractiveRebase">M512 64A447 447 0 0064 512c0 248 200 448 448 448s448-200 448-448S760 64 512 64zM218 295h31c54 0 105 19 145 55 13 12 13 31 3 43a35 35 0 01-22 10 36 36 0 01-21-7 155 155 0 00-103-39h-31a32 32 0 01-31-31c0-18 13-31 30-31zm31 433h-31a32 32 0 01-31-31c0-16 13-31 31-31h31A154 154 0 00403 512 217 217 0 01620 295h75l-93-67a33 33 0 01-7-43 33 33 0 0143-7l205 148-205 148a29 29 0 01-18 6 32 32 0 01-31-31c0-10 4-19 13-25l93-67H620a154 154 0 00-154 154c0 122-97 220-217 220zm390 118a29 29 0 01-18 6 32 32 0 01-31-31c0-10 4-19 13-25l93-67h-75c-52 0-103-19-143-54-12-12-13-31-1-43a30 30 0 0142-3 151 151 0 00102 39h75L602 599a33 33 0 01-7-43 33 33 0 0143-7l205 148-203 151z</StreamGeometry>
<StreamGeometry x:Key="Icons.Issue">M922 39H102A65 65 0 0039 106v609a65 65 0 0063 68h94v168a34 34 0 0019 31 30 30 0 0012 3 30 30 0 0022-10l182-192H922a65 65 0 0063-68V106A65 65 0 00922 39zM288 378h479a34 34 0 010 68H288a34 34 0 010-68zm0-135h479a34 34 0 010 68H288a34 34 0 010-68zm0 270h310a34 34 0 010 68H288a34 34 0 010-68z</StreamGeometry>
<StreamGeometry x:Key="Icons.Password">M640 96c-158 0-288 130-288 288 0 17 3 31 5 46L105 681 96 691V928h224v-96h96v-96h96v-95c38 18 82 31 128 31 158 0 288-130 288-288s-130-288-288-288zm0 64c123 0 224 101 224 224s-101 224-224 224a235 235 0 01-109-28l-8-4H448v96h-96v96H256v96H160v-146l253-254 12-11-3-17C419 417 416 400 416 384c0-123 101-224 224-224zm64 96a64 64 0 100 128 64 64 0 100-128z</StreamGeometry>
<StreamGeometry x:Key="Icons.LayoutHorizontal">M875 117H149C109 117 75 151 75 192v640c0 41 34 75 75 75h725c41 0 75-34 75-75V192c0-41-34-75-75-75zM139 832V192c0-6 4-11 11-11h331v661H149c-6 0-11-4-11-11zm747 0c0 6-4 11-11 11H544v-661H875c6 0 11 4 11 11v640z</StreamGeometry>
<StreamGeometry x:Key="Icons.LayoutVertical">M875 117H149C109 117 75 151 75 192v640c0 41 34 75 75 75h725c41 0 75-34 75-75V192c0-41-34-75-75-75zm-725 64h725c6 0 11 4 11 11v288h-747V192c0-6 4-11 11-11zm725 661H149c-6 0-11-4-11-11V544h747V832c0 6-4 11-11 11z</StreamGeometry>

View file

@ -124,6 +124,15 @@
<x:String x:Key="Text.Configure" xml:space="preserve">Repository Configure</x:String>
<x:String x:Key="Text.Configure.Email" xml:space="preserve">Email Address</x:String>
<x:String x:Key="Text.Configure.Email.Placeholder" xml:space="preserve">Email address</x:String>
<x:String x:Key="Text.Configure.Git" xml:space="preserve">GIT</x:String>
<x:String x:Key="Text.Configure.IssueTracker" xml:space="preserve">ISSUE TRACKER</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleGithub" xml:space="preserve">Add Sample Github Rule</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleJira" xml:space="preserve">Add Sample Jira Rule</x:String>
<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.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.Proxy" xml:space="preserve">HTTP Proxy</x:String>
<x:String x:Key="Text.Configure.Proxy.Placeholder" xml:space="preserve">HTTP proxy used by this repository</x:String>
<x:String x:Key="Text.Configure.User" xml:space="preserve">User Name</x:String>

View file

@ -127,6 +127,15 @@
<x:String x:Key="Text.Configure" xml:space="preserve">仓库配置</x:String>
<x:String x:Key="Text.Configure.Email" xml:space="preserve">电子邮箱</x:String>
<x:String x:Key="Text.Configure.Email.Placeholder" xml:space="preserve">邮箱地址</x:String>
<x:String x:Key="Text.Configure.Git" xml:space="preserve">GIT配置</x:String>
<x:String x:Key="Text.Configure.IssueTracker" xml:space="preserve">ISSUE追踪</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleGithub" xml:space="preserve">新增匹配Github Issue规则</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleJira" xml:space="preserve">新增匹配Jira规则</x:String>
<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.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.Proxy" xml:space="preserve">HTTP代理</x:String>
<x:String x:Key="Text.Configure.Proxy.Placeholder" xml:space="preserve">HTTP网络代理</x:String>
<x:String x:Key="Text.Configure.User" xml:space="preserve">用户名</x:String>

View file

@ -127,6 +127,15 @@
<x:String x:Key="Text.Configure" xml:space="preserve">倉庫配置</x:String>
<x:String x:Key="Text.Configure.Email" xml:space="preserve">電子郵箱</x:String>
<x:String x:Key="Text.Configure.Email.Placeholder" xml:space="preserve">郵箱地址</x:String>
<x:String x:Key="Text.Configure.Git" xml:space="preserve">GIT配置</x:String>
<x:String x:Key="Text.Configure.IssueTracker" xml:space="preserve">ISSUE追蹤</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleGithub" xml:space="preserve">新增匹配Github Issue規則</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleJira" xml:space="preserve">新增匹配Jira規則</x:String>
<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.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.Proxy" xml:space="preserve">HTTP代理</x:String>
<x:String x:Key="Text.Configure.Proxy.Placeholder" xml:space="preserve">HTTP網路代理</x:String>
<x:String x:Key="Text.Configure.User" xml:space="preserve">使用者名稱</x:String>

View file

@ -164,6 +164,7 @@
<Setter Property="Foreground" Value="{DynamicResource Brush.FG1}"/>
<Setter Property="Background" Value="{DynamicResource Brush.Popup}"/>
<Setter Property="VerticalOffset" Value="-8"/>
<Setter Property="TextBlock.TextDecorations" Value=""/>
<Setter Property="Template">
<ControlTemplate>
<Grid Effect="drop-shadow(0 0 8 #80000000)">
@ -276,6 +277,13 @@
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
<Style Selector="TextBlock.issue_link">
<Setter Property="Foreground" Value="{DynamicResource Brush.Accent}"/>
<Setter Property="Cursor" Value="Hand"/>
</Style>
<Style Selector="TextBlock.issue_link:pointerover">
<Setter Property="TextDecorations" Value="Underline"/>
</Style>
<Style Selector="SelectableTextBlock">
<Setter Property="HorizontalAlignment" Value="Left"/>

View file

@ -88,9 +88,15 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _viewRevisionFileContent, value);
}
public CommitDetail(string repo)
public IssueTrackerRuleSetting IssueTrackerSetting
{
get => _issueTrackerSetting;
}
public CommitDetail(string repo, IssueTrackerRuleSetting issueTrackerSetting)
{
_repo = repo;
_issueTrackerSetting = issueTrackerSetting;
}
public void Cleanup()
@ -242,7 +248,7 @@ namespace SourceGit.ViewModels
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, ev) =>
{
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, change.Path) };
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, change.Path, _issueTrackerSetting) };
window.Show();
ev.Handled = true;
};
@ -305,7 +311,7 @@ namespace SourceGit.ViewModels
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, ev) =>
{
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, file.Path) };
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, file.Path, _issueTrackerSetting) };
window.Show();
ev.Handled = true;
};
@ -457,6 +463,7 @@ namespace SourceGit.ViewModels
};
private string _repo;
private IssueTrackerRuleSetting _issueTrackerSetting = null;
private int _activePageIndex = 0;
private Models.Commit _commit = null;
private string _fullMessage = string.Empty;

View file

@ -54,11 +54,11 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _detailContext, value);
}
public FileHistories(string repo, string file)
public FileHistories(string repo, string file, IssueTrackerRuleSetting issueTrackerSetting)
{
_repo = repo;
_file = file;
_detailContext = new CommitDetail(repo);
_detailContext = new CommitDetail(repo, issueTrackerSetting);
Task.Run(() =>
{

View file

@ -94,7 +94,7 @@ namespace SourceGit.ViewModels
}
else
{
var commitDetail = new CommitDetail(_repo.FullPath);
var commitDetail = new CommitDetail(_repo.FullPath, _repo.IssueTrackerSetting);
commitDetail.Commit = commit;
DetailContext = commitDetail;
}
@ -122,7 +122,7 @@ namespace SourceGit.ViewModels
}
else
{
var commitDetail = new CommitDetail(_repo.FullPath);
var commitDetail = new CommitDetail(_repo.FullPath, _repo.IssueTrackerSetting);
commitDetail.Commit = commit;
DetailContext = commitDetail;
}

View file

@ -114,7 +114,7 @@ namespace SourceGit.ViewModels
Current = current;
On = on;
IsLoading = true;
DetailContext = new CommitDetail(repoPath);
DetailContext = new CommitDetail(repoPath, repo.IssueTrackerSetting);
Task.Run(() =>
{

View file

@ -0,0 +1,164 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class IssueTrackerMatch
{
public int Start { get; set; } = 0;
public int Length { get; set; } = 0;
public string URL { get; set; } = "";
public bool Intersect(int start, int length)
{
if (start == Start)
return true;
if (start < Start)
return start + length > Start;
return start < Start + Length;
}
}
public class IssueTrackerRule : ObservableObject
{
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
public string RegexString
{
get => _regexString;
set
{
if (SetProperty(ref _regexString, value))
{
try
{
_regex = null;
_regex = new Regex(_regexString, RegexOptions.Multiline);
}
catch
{
// Ignore errors.
}
}
OnPropertyChanged(nameof(IsRegexValid));
}
}
public bool IsRegexValid
{
get => _regex != null;
}
public string URLTemplate
{
get => _urlTemplate;
set => SetProperty(ref _urlTemplate, value);
}
public void Matches(List<IssueTrackerMatch> outs, string message)
{
if (_regex == null || string.IsNullOrEmpty(_urlTemplate))
return;
var matches = _regex.Matches(message);
for (int i = 0; i < matches.Count; i++)
{
var match = matches[i];
if (!match.Success)
continue;
var start = match.Index;
var len = match.Length;
var intersect = false;
foreach (var exist in outs)
{
if (exist.Intersect(start, len))
{
intersect = true;
break;
}
}
if (intersect)
continue;
var range = new IssueTrackerMatch();
range.Start = start;
range.Length = len;
range.URL = _urlTemplate;
for (int j = 1; j < match.Groups.Count; j++)
{
var group = match.Groups[j];
if (group.Success)
range.URL = range.URL.Replace($"${j}", group.Value);
}
outs.Add(range);
}
}
private string _name;
private string _regexString;
private string _urlTemplate;
private Regex _regex = null;
}
public class IssueTrackerRuleSetting
{
public AvaloniaList<IssueTrackerRule> Rules
{
get;
set;
} = new AvaloniaList<IssueTrackerRule>();
public IssueTrackerRule Add()
{
var rule = new IssueTrackerRule()
{
Name = "New Issue Tracker",
RegexString = "#(\\d+)",
URLTemplate = "https://xxx/$1",
};
Rules.Add(rule);
return rule;
}
public IssueTrackerRule AddGithub(string repoURL)
{
var rule = new IssueTrackerRule()
{
Name = "Github ISSUE",
RegexString = "#(\\d+)",
URLTemplate = string.IsNullOrEmpty(repoURL) ? "https://github.com/username/repository/issues/$1" : $"{repoURL}/issues/$1",
};
Rules.Add(rule);
return rule;
}
public IssueTrackerRule AddJira()
{
var rule = new IssueTrackerRule()
{
Name = "Jira Tracker",
RegexString = "PROJ-(\\d+)",
URLTemplate = "https://jira.yourcompany.com/browse/PROJ-$1",
};
Rules.Add(rule);
return rule;
}
}
}

View file

@ -44,6 +44,11 @@ namespace SourceGit.ViewModels
get => _settings;
}
public IssueTrackerRuleSetting IssueTrackerSetting
{
get => _issueTrackerSetting;
}
public int SelectedViewIndex
{
get => _selectedViewIndex;
@ -319,6 +324,23 @@ namespace SourceGit.ViewModels
_settings = new Models.RepositorySettings();
}
var issueTrackerSettingsFile = Path.Combine(_gitDir, "sourcegit.issuetracker.settings");
if (File.Exists(issueTrackerSettingsFile))
{
try
{
_issueTrackerSetting = JsonSerializer.Deserialize(File.ReadAllText(issueTrackerSettingsFile), JsonCodeGen.Default.IssueTrackerRuleSetting);
}
catch
{
_issueTrackerSetting = new IssueTrackerRuleSetting();
}
}
else
{
_issueTrackerSetting = new IssueTrackerRuleSetting();
}
_watcher = new Models.Watcher(this);
_histories = new Histories(this);
_workingCopy = new WorkingCopy(this);
@ -339,6 +361,10 @@ namespace SourceGit.ViewModels
File.WriteAllText(Path.Combine(_gitDir, "sourcegit.settings"), settingsSerialized);
_settings = null;
var issueTrackerSerialized = JsonSerializer.Serialize(_issueTrackerSetting, JsonCodeGen.Default.IssueTrackerRuleSetting);
File.WriteAllText(Path.Combine(_gitDir, "sourcegit.issuetracker.settings"), issueTrackerSerialized);
_issueTrackerSetting = null;
_watcher.Dispose();
_histories.Cleanup();
_workingCopy.Cleanup();
@ -1960,6 +1986,7 @@ namespace SourceGit.ViewModels
private string _fullpath = string.Empty;
private string _gitDir = string.Empty;
private Models.RepositorySettings _settings = null;
private IssueTrackerRuleSetting _issueTrackerSetting = null;
private Models.Watcher _watcher = null;
private Histories _histories = null;

View file

@ -1,4 +1,6 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
@ -41,6 +43,17 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _httpProxy, value);
}
public AvaloniaList<IssueTrackerRule> IssueTrackerRules
{
get => _repo.IssueTrackerSetting.Rules;
}
public IssueTrackerRule SelectedIssueTrackerRule
{
get => _selectedIssueTrackerRule;
set => SetProperty(ref _selectedIssueTrackerRule, value);
}
public RepositoryConfigure(Repository repo)
{
_repo = repo;
@ -65,6 +78,39 @@ namespace SourceGit.ViewModels
HttpProxy = string.Empty;
}
public void AddSampleGithubIssueTracker()
{
foreach (var remote in _repo.Remotes)
{
if (remote.URL.Contains("github.com", System.StringComparison.Ordinal))
{
if (remote.TryGetVisitURL(out string url))
{
SelectedIssueTrackerRule = _repo.IssueTrackerSetting.AddGithub(url);
return;
}
}
}
SelectedIssueTrackerRule = _repo.IssueTrackerSetting.AddGithub(null);
}
public void AddSampleJiraIssueTracker()
{
SelectedIssueTrackerRule = _repo.IssueTrackerSetting.AddJira();
}
public void NewIssueTracker()
{
SelectedIssueTrackerRule = _repo.IssueTrackerSetting.Add();
}
public void RemoveSelectedIssueTracker()
{
if (_selectedIssueTrackerRule != null)
_repo.IssueTrackerSetting.Rules.Remove(_selectedIssueTrackerRule);
}
public void Save()
{
SetIfChanged("user.name", UserName);
@ -96,5 +142,6 @@ namespace SourceGit.ViewModels
private readonly Repository _repo = null;
private readonly Dictionary<string, string> _cached = null;
private string _httpProxy;
private IssueTrackerRule _selectedIssueTrackerRule = null;
}
}

View file

@ -541,7 +541,7 @@ namespace SourceGit.ViewModels
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, e) =>
{
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo.FullPath, change.Path) };
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo.FullPath, change.Path, _repo.IssueTrackerSetting) };
window.Show();
e.Handled = true;
};

View file

@ -92,7 +92,12 @@
<!-- Messages -->
<TextBlock Grid.Row="3" Grid.Column="0" Classes="info_label" Text="{DynamicResource Text.CommitDetail.Info.Message}" VerticalAlignment="Top" Margin="0,4,0,0" />
<SelectableTextBlock Grid.Row="3" Grid.Column="1" Margin="12,5,8,0" Classes="primary" Text="{Binding #ThisControl.Message}" TextWrapping="Wrap"/>
<v:CommitMessagePresenter Grid.Row="3" Grid.Column="1"
Margin="12,5,8,0"
Classes="primary"
Message="{Binding #ThisControl.Message}"
IssueTrackerSetting="{Binding #ThisControl.IssueTrackerSetting}"
TextWrapping="Wrap"/>
</Grid>
</StackPanel>
</DataTemplate>

View file

@ -24,6 +24,15 @@ namespace SourceGit.Views
set => SetValue(MessageProperty, value);
}
public static readonly StyledProperty<ViewModels.IssueTrackerRuleSetting> IssueTrackerSettingProperty =
AvaloniaProperty.Register<CommitBaseInfo, ViewModels.IssueTrackerRuleSetting>(nameof(IssueTrackerSetting));
public ViewModels.IssueTrackerRuleSetting IssueTrackerSetting
{
get => GetValue(IssueTrackerSettingProperty);
set => SetValue(IssueTrackerSettingProperty, value);
}
public CommitBaseInfo()
{
InitializeComponent();

View file

@ -19,7 +19,9 @@
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel Orientation="Vertical">
<!-- Base Information -->
<v:CommitBaseInfo Content="{Binding Commit}" Message="{Binding FullMessage}"/>
<v:CommitBaseInfo Content="{Binding Commit}"
Message="{Binding FullMessage}"
IssueTrackerSetting="{Binding IssueTrackerSetting}"/>
<!-- Line -->
<Rectangle Height=".65" Margin="8" Fill="{DynamicResource Brush.Border2}"/>

View file

@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Input;
namespace SourceGit.Views
{
public class CommitMessagePresenter : SelectableTextBlock
{
public static readonly StyledProperty<string> MessageProperty =
AvaloniaProperty.Register<CommitMessagePresenter, string>(nameof(Message));
public string Message
{
get => GetValue(MessageProperty);
set => SetValue(MessageProperty, value);
}
public static readonly StyledProperty<ViewModels.IssueTrackerRuleSetting> IssueTrackerSettingProperty =
AvaloniaProperty.Register<CommitMessagePresenter, ViewModels.IssueTrackerRuleSetting>(nameof(IssueTrackerSetting));
public ViewModels.IssueTrackerRuleSetting IssueTrackerSetting
{
get => GetValue(IssueTrackerSettingProperty);
set => SetValue(IssueTrackerSettingProperty, value);
}
protected override Type StyleKeyOverride => typeof(SelectableTextBlock);
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == MessageProperty || change.Property == IssueTrackerSettingProperty)
{
Inlines.Clear();
var message = Message;
if (string.IsNullOrEmpty(message))
return;
var rules = IssueTrackerSetting?.Rules;
if (rules == null || rules.Count == 0)
{
Inlines.Add(new Run(message));
return;
}
var matches = new List<ViewModels.IssueTrackerMatch>();
foreach (var rule in rules)
rule.Matches(matches, message);
if (matches.Count == 0)
{
Inlines.Add(new Run(message));
return;
}
matches.Sort((l, r) => l.Start - r.Start);
int pos = 0;
foreach (var match in matches)
{
if (match.Start > pos)
Inlines.Add(new Run(message.Substring(pos, match.Start - pos)));
var link = new TextBlock();
link.SetValue(TextProperty, message.Substring(match.Start, match.Length));
link.SetValue(ToolTip.TipProperty, match.URL);
link.Classes.Add("issue_link");
link.PointerPressed += OnLinkPointerPressed;
Inlines.Add(link);
pos = match.Start + match.Length;
}
if (pos < message.Length)
Inlines.Add(new Run(message.Substring(pos)));
}
}
private void OnLinkPointerPressed(object sender, PointerPressedEventArgs e)
{
if (sender is TextBlock text)
{
var tooltip = text.GetValue(ToolTip.TipProperty) as string;
if (!string.IsNullOrEmpty(tooltip))
Native.OS.OpenBrowser(tooltip);
e.Handled = true;
}
}
}
}

View file

@ -47,7 +47,13 @@
</Grid>
<!-- Body -->
<Grid Grid.Row="1" Margin="16,8,16,0" RowDefinitions="32,32,32,32,32,32" ColumnDefinitions="Auto,*">
<TabControl Grid.Row="1">
<TabItem>
<TabItem.Header>
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Configure.Git}"/>
</TabItem.Header>
<Grid Margin="16,4,16,8" RowDefinitions="32,32,32,32,32,32" ColumnDefinitions="Auto,*">
<TextBlock Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
@ -103,23 +109,105 @@
Content="{DynamicResource Text.Preference.GPG.TagEnabled}"
IsChecked="{Binding GPGTagSigningEnabled, Mode=TwoWay}"/>
</Grid>
</TabItem>
<!-- Options -->
<StackPanel Grid.Row="2"
Margin="8,4,8,8"
Height="32"
Orientation="Horizontal"
HorizontalAlignment="Center">
<Button Classes="flat primary"
Width="80"
Content="{DynamicResource Text.Sure}"
Click="SaveAndClose"
HotKey="Enter"/>
<Button Classes="flat"
Width="80"
Margin="8,0,0,0"
Content="{DynamicResource Text.Cancel}"
Click="CloseWindow"/>
<TabItem>
<TabItem.Header>
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Configure.IssueTracker}"/>
</TabItem.Header>
<Grid ColumnDefinitions="200,*" Height="250" Margin="0,8,0,16">
<Border Grid.Column="0"
BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}"
Background="{DynamicResource Brush.Contents}">
<Grid RowDefinitions="*,1,Auto">
<ListBox Grid.Row="0"
Background="Transparent"
ItemsSource="{Binding IssueTrackerRules}"
SelectedItem="{Binding SelectedIssueTrackerRule, Mode=TwoWay}"
SelectionMode="AlwaysSelected,Single">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="MinHeight" Value="0"/>
<Setter Property="Height" Value="26"/>
<Setter Property="Padding" Value="4,2"/>
</Style>
</ListBox.Styles>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="vm:IssueTrackerRule">
<TextBlock Grid.Column="1" Text="{Binding Name}" Margin="8,0" TextTrimming="CharacterEllipsis"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Rectangle Grid.Row="1" Height="1" Fill="{DynamicResource Brush.Border2}" HorizontalAlignment="Stretch" VerticalAlignment="Bottom"/>
<StackPanel Grid.Row="2" Orientation="Horizontal" Background="{DynamicResource Brush.ToolBar}">
<Button Classes="icon_button">
<Button.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedLeft">
<MenuItem Header="{DynamicResource Text.Configure.IssueTracker.NewRule}" Command="{Binding NewIssueTracker}"/>
<MenuItem Header="-"/>
<MenuItem Header="{DynamicResource Text.Configure.IssueTracker.AddSampleGithub}" Command="{Binding AddSampleGithubIssueTracker}"/>
<MenuItem Header="{DynamicResource Text.Configure.IssueTracker.AddSampleJira}" Command="{Binding AddSampleJiraIssueTracker}"/>
</MenuFlyout>
</Button.Flyout>
<Path Width="14" Height="14" Data="{StaticResource Icons.Plus}"/>
</Button>
<Rectangle Grid.Row="1" Width="1" Fill="{DynamicResource Brush.Border2}" HorizontalAlignment="Left" VerticalAlignment="Stretch"/>
<Button Classes="icon_button" Command="{Binding RemoveSelectedIssueTracker}">
<Path Width="14" Height="14" Data="{StaticResource Icons.Window.Minimize}"/>
</Button>
<Rectangle Grid.Row="1" Width="1" Fill="{DynamicResource Brush.Border2}" HorizontalAlignment="Left" VerticalAlignment="Stretch"/>
</StackPanel>
</Grid>
</Border>
<ContentControl Grid.Column="1" Content="{Binding SelectedIssueTrackerRule}" Margin="16,0,0,0">
<ContentControl.Content>
<Binding Path="SelectedIssueTrackerRule">
<Binding.TargetNullValue>
<Path Width="64" Height="64"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Fill="{DynamicResource Brush.FG2}"
Data="{StaticResource Icons.Issue}"/>
</Binding.TargetNullValue>
</Binding>
</ContentControl.Content>
<ContentControl.DataTemplates>
<DataTemplate DataType="vm:IssueTrackerRule">
<Grid Grid.Column="1" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<TextBlock Grid.Row="0" Text="{DynamicResource Text.Configure.IssueTracker.RuleName}"/>
<TextBox Grid.Row="1" CornerRadius="3" Height="28" Text="{Binding Name, Mode=TwoWay}"/>
<TextBlock Grid.Row="2" Margin="0,12,0,0" Text="{DynamicResource Text.Configure.IssueTracker.Regex}"/>
<TextBox Grid.Row="3" CornerRadius="3" Height="28" Text="{Binding RegexString, Mode=TwoWay}">
<TextBox.InnerRightContent>
<Path Margin="4,0" Width="12" Height="12" Data="{StaticResource Icons.Error}" Fill="OrangeRed" IsVisible="{Binding !IsRegexValid}"/>
</TextBox.InnerRightContent>
</TextBox>
<TextBlock Grid.Row="4" Margin="0,12,0,0" Text="{DynamicResource Text.Configure.IssueTracker.URLTemplate}"/>
<TextBox Grid.Row="5" CornerRadius="3" Height="28" Text="{Binding URLTemplate, Mode=TwoWay}"/>
<TextBlock Grid.Row="6" Text="{DynamicResource Text.Configure.IssueTracker.URLTemplate.Tip}" Foreground="{DynamicResource Brush.FG2}"/>
</Grid>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
</Grid>
</TabItem>
</TabControl>
</Grid>
</v:ChromelessWindow>

View file

@ -16,11 +16,6 @@ namespace SourceGit.Views
}
private void CloseWindow(object _1, RoutedEventArgs _2)
{
Close();
}
private void SaveAndClose(object _1, RoutedEventArgs _2)
{
(DataContext as ViewModels.RepositoryConfigure)?.Save();
Close();