mirror of
https://gitlab.gnome.org/GNOME/gitg
synced 2024-10-02 14:13:48 +00:00
Detect links in commit messages
Parse subject and body for a commit message and convert urls into links. It is possible to read custom links config from ./git/config ini file: example to parse issues and merge request for gitlab: ``` [gitg.custom-link "issues"] regexp=!([0-9]+) replacement=https://gitlab.gnome.org/GNOME/gitg/merge_requests/\1 color = orange [gitg.custom-link "merge.request"] regexp=#([0-9]+) replacement=https://gitlab.gnome.org/GNOME/gitg/merge_requests/\1 color = green ```
This commit is contained in:
parent
cfb68e1628
commit
79a10fbf87
|
@ -117,10 +117,7 @@ class Gitg.DiffViewCommitDetails : Gtk.Grid
|
|||
}
|
||||
}
|
||||
|
||||
public DiffViewCommitDetails(Ggit.Commit? commit)
|
||||
{
|
||||
Object(commit: commit);
|
||||
}
|
||||
public string config_file { get; construct set; }
|
||||
|
||||
private bool d_use_gravatar;
|
||||
|
||||
|
@ -136,6 +133,8 @@ class Gitg.DiffViewCommitDetails : Gtk.Grid
|
|||
|
||||
private Gee.HashMap<Ggit.OId, Gtk.RadioButton> d_parents_map;
|
||||
|
||||
private GLib.Regex regex_url = /\w+:(\/?\/?)[^\s]+/;
|
||||
|
||||
construct
|
||||
{
|
||||
d_expander_files.notify["expanded"].connect(() => {
|
||||
|
@ -186,7 +185,7 @@ class Gitg.DiffViewCommitDetails : Gtk.Grid
|
|||
return;
|
||||
}
|
||||
|
||||
d_label_subject.label = commit.get_subject();
|
||||
d_label_subject.label = parse_links_on_subject(commit.get_subject());
|
||||
d_label_sha1.label = commit.get_id().to_string();
|
||||
|
||||
var author = commit.get_author();
|
||||
|
@ -257,6 +256,92 @@ class Gitg.DiffViewCommitDetails : Gtk.Grid
|
|||
update_avatar();
|
||||
}
|
||||
|
||||
private string parse_links_on_subject(string subject_text)
|
||||
{
|
||||
string result = subject_text.dup();
|
||||
try
|
||||
{
|
||||
GLib.MatchInfo matchInfo;
|
||||
regex_url.match (subject_text, 0, out matchInfo);
|
||||
|
||||
while (matchInfo.matches ())
|
||||
{
|
||||
string text = matchInfo.fetch(0);
|
||||
result = result.replace(text, "<a href=\"%s\">%s</a>".printf(text, text));
|
||||
matchInfo.next();
|
||||
}
|
||||
|
||||
result = parse_ini_file(result);
|
||||
}
|
||||
catch(Error e)
|
||||
{
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private string parse_ini_file(string subject_text)
|
||||
{
|
||||
string result = subject_text.dup();
|
||||
GLib.KeyFile file = new GLib.KeyFile();
|
||||
|
||||
try
|
||||
{
|
||||
debug ("parsing %s", config_file);
|
||||
if (file.load_from_file(config_file , GLib.KeyFileFlags.NONE))
|
||||
{
|
||||
foreach (string group in file.get_groups())
|
||||
{
|
||||
if (group.has_prefix("gitg.custom-link"))
|
||||
{
|
||||
string custom_link_regexp = file.get_string (group, "regexp");
|
||||
string custom_link_replacement = file.get_string (group, "replacement");
|
||||
debug ("found group: %s", custom_link_regexp);
|
||||
bool custom_color = file.has_key (group, "color");
|
||||
string color = null;
|
||||
if (custom_color)
|
||||
{
|
||||
string custom_link_color = file.get_string (group, "color");
|
||||
color = custom_link_color;
|
||||
}
|
||||
|
||||
var custom_regex = new Regex (custom_link_regexp);
|
||||
try
|
||||
{
|
||||
GLib.MatchInfo matchInfo;
|
||||
|
||||
custom_regex.match (subject_text, 0, out matchInfo);
|
||||
|
||||
while (matchInfo.matches ())
|
||||
{
|
||||
string text = matchInfo.fetch(0);
|
||||
string link = text.dup();
|
||||
debug ("found: %s", link);
|
||||
if (custom_link_replacement != null)
|
||||
{
|
||||
link = custom_regex.replace(link, text.length, 0, custom_link_replacement);
|
||||
}
|
||||
if (color != null) {
|
||||
result = result.replace(text, "<a href=\"%s\" title=\"%s\" style=\"color:%s\">%s</a>".printf(link, link, color, text));
|
||||
} else {
|
||||
result = result.replace(text, "<a href=\"%s\" title=\"%s\">%s</a>".printf(link, link, text));
|
||||
}
|
||||
|
||||
matchInfo.next();
|
||||
}
|
||||
}
|
||||
catch(Error e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Error e)
|
||||
{
|
||||
warning ("Cannot read %s %s", config_file, e.message);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void update_avatar()
|
||||
{
|
||||
if (commit == null)
|
||||
|
|
|
@ -49,6 +49,11 @@ public class Gitg.DiffView : Gtk.Grid
|
|||
private ulong d_parent_commit_notify;
|
||||
private bool d_changes_inline;
|
||||
|
||||
Gdk.RGBA d_color_link;
|
||||
Gdk.RGBA color_hovered_link;
|
||||
bool hovering_over_link = false;
|
||||
Gtk.TextTag hover_tag = null;
|
||||
|
||||
private uint d_reveal_options_timeout;
|
||||
private uint d_unreveal_options_timeout;
|
||||
|
||||
|
@ -119,8 +124,21 @@ public class Gitg.DiffView : Gtk.Grid
|
|||
public int tab_width { get; construct set; default = 4; }
|
||||
public bool handle_selection { get; construct set; default = false; }
|
||||
public bool highlight { get; construct set; default = true; }
|
||||
public Repository repository { get; set; }
|
||||
|
||||
private Repository? d_repository;
|
||||
|
||||
public Repository? repository {
|
||||
get { return d_repository; }
|
||||
set {
|
||||
d_repository = value;
|
||||
config_file = "%s/.git/config".printf(d_repository.get_workdir().get_path());
|
||||
d_commit_details.config_file = config_file;
|
||||
}
|
||||
}
|
||||
public bool new_is_workdir { get; set; }
|
||||
private string config_file;
|
||||
|
||||
private GLib.Regex regex_url = /\w+:(\/?\/?)[^\s]+/;
|
||||
|
||||
private bool flag_get(Ggit.DiffOption f)
|
||||
{
|
||||
|
@ -190,6 +208,14 @@ public class Gitg.DiffView : Gtk.Grid
|
|||
d_parent_commit_notify = d_commit_details.notify["parent-commit"].connect(parent_commit_changed);
|
||||
|
||||
bind_property("use-gravatar", d_commit_details, "use-gravatar", BindingFlags.SYNC_CREATE);
|
||||
d_text_view_message.event_after.connect (on_event_after);
|
||||
d_text_view_message.key_press_event.connect (on_key_press);
|
||||
d_text_view_message.motion_notify_event.connect (on_motion_notify_event);
|
||||
d_text_view_message.has_tooltip = true;
|
||||
d_text_view_message.query_tooltip.connect (on_query_tooltip_event);
|
||||
d_text_view_message.style_updated.connect (load_colors_from_theme);
|
||||
|
||||
load_colors_from_theme(d_text_view_message);
|
||||
|
||||
d_event_box.motion_notify_event.connect(motion_notify_event_on_event_box);
|
||||
d_diff_view_options.view = this;
|
||||
|
@ -258,8 +284,244 @@ public class Gitg.DiffView : Gtk.Grid
|
|||
return "";
|
||||
}
|
||||
|
||||
private bool on_query_tooltip_event(int x, int y, bool keyboard_tooltip, Gtk.Tooltip tooltip)
|
||||
{
|
||||
Gtk.TextIter iter;
|
||||
|
||||
if (d_text_view_message.get_iter_at_location (out iter, x, y))
|
||||
{
|
||||
var tags = iter.get_tags ();
|
||||
foreach (Gtk.TextTag tag in tags)
|
||||
{
|
||||
if (tag.get_data<string>("type") == "url" && tag.get_data<bool>("is_custom_link"))
|
||||
{
|
||||
string url = tag.get_data<string>("url");
|
||||
tooltip.set_text (url);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void apply_link_tags(Gtk.TextBuffer buffer, Regex regex, string? replacement, Gdk.RGBA custom_color_link, bool is_custom_color, bool is_custom_link)
|
||||
{
|
||||
try
|
||||
{
|
||||
GLib.MatchInfo matchInfo;
|
||||
|
||||
var buffer_text = buffer.text;
|
||||
regex.match (buffer_text, 0, out matchInfo);
|
||||
|
||||
while (matchInfo.matches ())
|
||||
{
|
||||
Gtk.TextIter start, end;
|
||||
int start_pos, end_pos;
|
||||
string text = matchInfo.fetch(0);
|
||||
matchInfo.fetch_pos (0, out start_pos, out end_pos);
|
||||
buffer.get_iter_at_offset(out start, start_pos);
|
||||
buffer.get_iter_at_offset(out end, end_pos);
|
||||
|
||||
var tag = buffer.create_tag(null, "underline", Pango.Underline.SINGLE);
|
||||
tag.foreground_rgba = custom_color_link;
|
||||
tag.set_data("type", "url");
|
||||
tag.set_data<Gdk.RGBA?>("color_link", custom_color_link);
|
||||
if (replacement != null)
|
||||
{
|
||||
text = regex.replace(text, text.length, 0, replacement);
|
||||
}
|
||||
tag.set_data("url", text);
|
||||
tag.set_data("is_custom_color_link", is_custom_color);
|
||||
tag.set_data("is_custom_link", is_custom_link);
|
||||
buffer.apply_tag(tag, start, end);
|
||||
|
||||
matchInfo.next();
|
||||
}
|
||||
}
|
||||
catch(Error e)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void load_colors_from_theme(Gtk.Widget widget)
|
||||
{
|
||||
Gtk.TextView textview = (Gtk.TextView)widget;
|
||||
Gtk.StyleContext context = textview.get_style_context ();
|
||||
|
||||
context.save ();
|
||||
context.set_state (Gtk.StateFlags.LINK);
|
||||
d_color_link = context.get_color (context.get_state ());
|
||||
|
||||
context.set_state (Gtk.StateFlags.LINK | Gtk.StateFlags.PRELIGHT);
|
||||
color_hovered_link = context.get_color (context.get_state ());
|
||||
context.restore ();
|
||||
|
||||
textview.buffer.tag_table.foreach ((tag) =>
|
||||
{
|
||||
if (!tag.get_data<bool>("is_custom_color_link"))
|
||||
{
|
||||
tag.set_data<Gdk.RGBA?>("color_link", d_color_link);
|
||||
tag.foreground_rgba = d_color_link;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private bool on_key_press (Gtk.Widget widget, Gdk.EventKey evt)
|
||||
{
|
||||
if (evt.keyval == Gdk.Key.Return || evt.keyval == Gdk.Key.KP_Enter)
|
||||
{
|
||||
|
||||
Gtk.TextIter iter;
|
||||
Gtk.TextView textview = (Gtk.TextView) widget;
|
||||
textview.buffer.get_iter_at_mark(out iter, textview.buffer.get_insert());
|
||||
|
||||
follow_if_link (widget, iter);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void follow_if_link(Gtk.Widget texview, Gtk.TextIter iter)
|
||||
{
|
||||
var tags = iter.get_tags ();
|
||||
foreach (Gtk.TextTag tag in tags)
|
||||
{
|
||||
if (tag.get_data<string>("type") == "url")
|
||||
{
|
||||
string url = tag.get_data<string>("url");
|
||||
try
|
||||
{
|
||||
GLib.AppInfo.launch_default_for_uri(url, null);
|
||||
}
|
||||
catch(Error e)
|
||||
{
|
||||
warning ("Cannot open %s: %s", url, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void on_event_after (Gtk.Widget widget, Gdk.Event evt)
|
||||
{
|
||||
|
||||
Gtk.TextIter start, end, iter;
|
||||
Gtk.TextBuffer buffer;
|
||||
double ex, ey;
|
||||
int x, y;
|
||||
|
||||
if (evt.type == Gdk.EventType.BUTTON_RELEASE)
|
||||
{
|
||||
Gdk.EventButton event;
|
||||
|
||||
event = (Gdk.EventButton)evt;
|
||||
if (event.button != Gdk.BUTTON_PRIMARY)
|
||||
return;
|
||||
|
||||
ex = event.x;
|
||||
ey = event.y;
|
||||
}
|
||||
else if (evt.type == Gdk.EventType.TOUCH_END)
|
||||
{
|
||||
Gdk.EventTouch event;
|
||||
|
||||
event = (Gdk.EventTouch)evt;
|
||||
|
||||
ex = event.x;
|
||||
ey = event.y;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Gtk.TextView textview = (Gtk.TextView)widget;
|
||||
buffer = textview.buffer;
|
||||
|
||||
/* we shouldn't follow a link if the user has selected something */
|
||||
buffer.get_selection_bounds (out start, out end);
|
||||
if (start.get_offset () != end.get_offset ())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
textview.window_to_buffer_coords (Gtk.TextWindowType.WIDGET,(int)ex, (int)ey, out x, out y);
|
||||
|
||||
if (textview.get_iter_at_location (out iter, x, y))
|
||||
{
|
||||
follow_if_link (textview, iter);
|
||||
}
|
||||
}
|
||||
|
||||
private bool on_motion_notify_event (Gtk.Widget widget, Gdk.EventMotion evt)
|
||||
{
|
||||
int x, y;
|
||||
|
||||
Gtk.TextView textview = ((Gtk.TextView)widget);
|
||||
|
||||
textview.window_to_buffer_coords (Gtk.TextWindowType.WIDGET,(int)evt.x, (int)evt.y, out x, out y);
|
||||
|
||||
Gtk.TextIter iter;
|
||||
bool hovering = false;
|
||||
|
||||
if (textview.get_iter_at_location (out iter, x, y))
|
||||
{
|
||||
var tags = iter.get_tags ();
|
||||
foreach (Gtk.TextTag tag in tags)
|
||||
{
|
||||
if (tag.get_data<string>("type") == "url")
|
||||
{
|
||||
hovering = true;
|
||||
if (hover_tag != null && hover_tag != tag)
|
||||
{
|
||||
restore_tag_color_link (hover_tag);
|
||||
hovering_over_link = false;
|
||||
}
|
||||
hover_tag = tag;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hovering != hovering_over_link)
|
||||
{
|
||||
hovering_over_link = hovering;
|
||||
|
||||
Gdk.Display display = textview.get_display();
|
||||
Gdk.Cursor hand_cursor = new Gdk.Cursor.from_name (display, "pointer");
|
||||
Gdk.Cursor regular_cursor = new Gdk.Cursor.from_name (display, "text");
|
||||
|
||||
Gdk.Window window = textview.get_window (Gtk.TextWindowType.TEXT);
|
||||
if (hovering_over_link)
|
||||
{
|
||||
window.set_cursor (hand_cursor);
|
||||
if (hover_tag != null)
|
||||
{
|
||||
hover_tag.foreground_rgba = color_hovered_link;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
window.set_cursor (regular_cursor);
|
||||
if (hover_tag != null)
|
||||
{
|
||||
restore_tag_color_link (hover_tag);
|
||||
hover_tag = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void restore_tag_color_link (Gtk.TextTag tag)
|
||||
{
|
||||
Gdk.RGBA? color = tag.get_data<Gdk.RGBA?>("color_link");
|
||||
tag.foreground_rgba = color;
|
||||
}
|
||||
|
||||
private void update(bool preserve_expanded)
|
||||
{
|
||||
|
||||
// If both `d_diff` and `d_commit` are null, clear
|
||||
// the diff content
|
||||
if (d_diff == null && d_commit == null)
|
||||
|
@ -307,6 +569,12 @@ public class Gitg.DiffView : Gtk.Grid
|
|||
var message = message_without_subject(d_commit);
|
||||
|
||||
d_text_view_message.buffer.set_text(message);
|
||||
var buffer = d_text_view_message.get_buffer();
|
||||
|
||||
apply_link_tags(buffer, regex_url, null, d_color_link, false, false);
|
||||
|
||||
read_ini_file(buffer);
|
||||
|
||||
d_text_view_message.visible = (message != "");
|
||||
}
|
||||
else
|
||||
|
@ -323,6 +591,38 @@ public class Gitg.DiffView : Gtk.Grid
|
|||
}
|
||||
}
|
||||
|
||||
private void read_ini_file(Gtk.TextBuffer buffer)
|
||||
{
|
||||
GLib.KeyFile file = new GLib.KeyFile();
|
||||
|
||||
try
|
||||
{
|
||||
if (file.load_from_file(config_file , GLib.KeyFileFlags.NONE))
|
||||
{
|
||||
foreach (string group in file.get_groups())
|
||||
{
|
||||
if (group.has_prefix("gitg.custom-link"))
|
||||
{
|
||||
string custom_link_regexp = file.get_string (group, "regexp");
|
||||
string custom_link_replacement = file.get_string (group, "replacement");
|
||||
bool custom_color = file.has_key (group, "color");
|
||||
Gdk.RGBA color = d_color_link;
|
||||
if (custom_color)
|
||||
{
|
||||
string custom_link_color = file.get_string (group, "color");
|
||||
color = Gdk.RGBA();
|
||||
color.parse(custom_link_color);
|
||||
}
|
||||
apply_link_tags(buffer, new Regex (custom_link_regexp), custom_link_replacement, color, custom_color, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Error e)
|
||||
{
|
||||
warning ("Cannot read %s: %s", config_file, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
private void auto_change_expanded(bool expanded)
|
||||
{
|
||||
SignalHandler.block(d_commit_details, d_expanded_notify);
|
||||
|
|
|
@ -130,6 +130,7 @@
|
|||
<property name="wrap">True</property>
|
||||
<property name="selectable">True</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="use_markup">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
|
|
Loading…
Reference in a new issue