[gitg] Detect links in commit messages



commit 79a10fbf87e39bf1d7a59161d4be6e9f4e7e171a
Author: Gaurav Agrawal <agrawalgaurav1999 gmail com>
Date:   Mon May 20 00:30:15 2019 +0200

    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
    ```

 libgitg/gitg-diff-view-commit-details.vala         |  95 ++++++-
 libgitg/gitg-diff-view.vala                        | 302 ++++++++++++++++++++-
 .../resources/ui/gitg-diff-view-commit-details.ui  |   1 +
 3 files changed, 392 insertions(+), 6 deletions(-)
---
diff --git a/libgitg/gitg-diff-view-commit-details.vala b/libgitg/gitg-diff-view-commit-details.vala
index 254844e9..f90f1e41 100644
--- a/libgitg/gitg-diff-view-commit-details.vala
+++ b/libgitg/gitg-diff-view-commit-details.vala
@@ -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)
diff --git a/libgitg/gitg-diff-view.vala b/libgitg/gitg-diff-view.vala
index 8680f44b..b5ae0c8b 100644
--- a/libgitg/gitg-diff-view.vala
+++ b/libgitg/gitg-diff-view.vala
@@ -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);
diff --git a/libgitg/resources/ui/gitg-diff-view-commit-details.ui 
b/libgitg/resources/ui/gitg-diff-view-commit-details.ui
index 70c3a368..dee13bea 100644
--- a/libgitg/resources/ui/gitg-diff-view-commit-details.ui
+++ b/libgitg/resources/ui/gitg-diff-view-commit-details.ui
@@ -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>


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]