Subject: [PATCH 1/4] Inotify Support

From: Florian Schüller <[email-masked]>
Date: Wed, 8 Mar 2017 21:52:52 +0100

Just automatically update gitk when working in a terminal on the same repo

Features:
* Detects inotify support
  if inotify is not detected the options is not available
  in the preferences
* Enable/Disable auto update in the preferences
* Select "debounce" time for redraw
  i.e. the redraw will be postponed for the given time.
  if a new change is detected in this time the redraw is postponed
  even more
* Automatically scroll to the new HEAD after redrawing
* Depending on the type of change the UI is "Updated" or "Reloaded"

Open points for now:
* release watches for deleted directories seems to
  cause problems in tcl-inotify (so I don't)
  I'm not sure how often that happens in ".git/"

Signed-off-by: Florian Schüller <[email-masked]>
---
 gitk | 171 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 170 insertions(+), 1 deletion(-)

diff --git a/gitk b/gitk
index a14d7a1..a2850d7 100755
--- a/gitk
+++ b/gitk
@@ -8,6 +8,12 @@ exec wish "$0" -- "$@"
 # either version 2, or (at your option) any later version.
 
 package require Tk
+try {
+    package require inotify
+    set have_inotify true
+} on error {em} {
+    set have_inotify false
+}
 
 proc hasworktree {} {
     return [expr {[exec git rev-parse --is-bare-repository] == "false" &&
@@ -11489,6 +11495,7 @@ proc prefspage_general {notebook} {
     global NS maxwidth maxgraphpct showneartags showlocalchanges
     global tabstop limitdiffs autoselect autosellen extdifftool perfile_attrs
     global hideremotes want_ttk have_ttk maxrefs
+    global autoupdate have_inotify autoupdatedebounce
 
     set page [create_prefs_page $notebook.general]
 
@@ -11505,13 +11512,21 @@ proc prefspage_general {notebook} {
     ${NS}::checkbutton $page.showlocal -text [mc "Show local changes"] \
 	-variable showlocalchanges
     grid x $page.showlocal -sticky w
+
     ${NS}::checkbutton $page.autoselect -text [mc "Auto-select SHA1 (length)"] \
 	-variable autoselect
     spinbox $page.autosellen -from 1 -to 40 -width 4 -textvariable autosellen
     grid x $page.autoselect $page.autosellen -sticky w
+
     ${NS}::checkbutton $page.hideremotes -text [mc "Hide remote refs"] \
 	-variable hideremotes
     grid x $page.hideremotes -sticky w
+    if { $have_inotify } {
+        ${NS}::checkbutton $page.autoupdate -text [mc "Auto-update upon change (ms)"] \
+            -variable autoupdate
+        spinbox $page.autoupdatedebounce -from 10 -to 60000 -width 7 -textvariable autoupdatedebounce
+        grid x $page.autoupdate $page.autoupdatedebounce -sticky w
+    }
 
     ${NS}::label $page.ddisp -text [mc "Diff display options"]
     grid $page.ddisp - -sticky w -pady 10
@@ -11765,7 +11780,8 @@ proc prefsok {} {
     global oldprefs prefstop showneartags showlocalchanges
     global fontpref mainfont textfont uifont
     global limitdiffs treediffs perfile_attrs
-    global hideremotes
+    global hideremotes autoupdate
+    global gitdir
 
     catch {destroy $prefstop}
     unset prefstop
@@ -11814,6 +11830,8 @@ proc prefsok {} {
     if {$hideremotes != $oldprefs(hideremotes)} {
 	rereadrefs
     }
+
+    handle_inotify $gitdir true
 }
 
 proc formatdate {d} {
@@ -12295,6 +12313,13 @@ set autoselect 1
 set autosellen 40
 set perfile_attrs 0
 set want_ttk 1
+set autoupdate 1
+set autoupdatedebounce 100
+#timer id for inotify reloading
+set reload_id -1
+#timer id for inotify updating (less than reload)
+set update_id -1
+set inotify_instance -1
 
 if {[tk windowingsystem] eq "aqua"} {
     set extdifftool "opendiff"
@@ -12390,6 +12415,7 @@ set config_variables {
     filesepbgcolor filesepfgcolor linehoverbgcolor linehoverfgcolor
     linehoveroutlinecolor mainheadcirclecolor workingfilescirclecolor
     indexcirclecolor circlecolors linkfgcolor circleoutlinecolor
+    autoupdate autoupdatedebounce
 }
 foreach var $config_variables {
     config_init_trace $var
@@ -12477,6 +12503,149 @@ if {$i >= [llength $argv] && $revtreeargs ne {}} {
     }
 }
 
+#function to be called after inotify reload-timeout
+proc reload_helper {} {
+    #puts "RELOAD"
+    global reload_id
+    set reload_id -1
+    reloadcommits
+    set head [exec git rev-parse HEAD]
+    selbyid $head
+}
+
+#function to be called after inotify update-timeout
+proc update_helper {} {
+    #puts "UPDATE"
+    global update_id
+    set update_id -1
+    updatecommits
+    set head [exec git rev-parse HEAD]
+    selbyid $head
+}
+
+proc inotify_handler { fd } {
+    global autoupdate reload_id update_id autoupdatedebounce
+    set events [inotify_watch read]
+    set watch_info [inotify_watch info]
+    set update_view false
+    set reloadcommits false
+
+    #cancel pending timer
+    if { $reload_id ne -1 } {
+        #puts "cancel a reload"
+        after cancel $reload_id
+        set reload_id -1
+        set update_view true
+        set reloadcommits true
+    }
+
+    if { $update_id ne -1 } {
+        #puts "cancel an update"
+        after cancel $update_id
+        set update_id -1
+        set update_view true
+    }
+
+    foreach {event} $events {
+        set current_watchid [dict get $event watchid]
+        set flags [dict get $event flags]
+        set event_filename [dict get $event filename]
+
+        foreach {path watchid watch_flags} $watch_info {
+            if {$watchid eq $current_watchid} {
+                set watch_path $path
+            }
+        }
+
+        set full_filename [file join $watch_path $event_filename]
+        #check wether we should do update or reload below
+        #puts "Got: $full_filename / $event_filename ($flags)"
+
+        if {$flags eq "nD"} {
+            inotify_watch add $full_filename "nwds"
+        }
+        if {![string match *.lock $event_filename]} {
+            if { $flags eq "d" } {
+                #stuff like deleting branches should result in reloading
+                set reloadcommits true
+            }
+            set update_view true
+        }
+
+        #simple commit just needs updating right?
+        #if { $event_filename eq "COMMIT_EDITMSG" } {
+        #    set reloadcommits true
+        #}
+    }
+
+    #reloadcommits or updatecommits - depending on file and operation?
+    if { $update_view } {
+        if { $reloadcommits } {
+            #puts "schedule reload"
+            set reload_id [after $autoupdatedebounce reload_helper]
+        } else {
+            #puts "schedule update"
+            set update_id [after $autoupdatedebounce update_helper]
+        }
+    }
+}
+
+proc watch_recursive { dir } {
+    inotify_watch add $dir "nwaCmMds"
+
+    foreach i [glob -nocomplain -dir $dir *] {
+        if {[file type $i] eq {directory}} {
+            watch_recursive $i
+        }
+    }
+}
+
+proc enable_inotify { dir redraw} {
+    global inotify_instance autoupdatedebounce reload_id
+
+    if { $inotify_instance ne -1 } {
+        updatecommits
+    } else {
+        set inotify_instance [inotify create "inotify_watch" "::inotify_handler"]
+        watch_recursive $dir
+        if { $redraw } {
+            set reload_id [after $autoupdatedebounce reload_helper]
+        }
+    }
+}
+
+proc disable_inotify {} {
+    global inotify_instance reload_id update_id
+
+    if { $inotify_instance ne -1 } {
+        rename inotify_watch {}
+        set inotify_instance -1
+    }
+
+    if { $reload_id ne -1 } {
+        after cancel $reload_id
+        set reload_id -1
+    }
+
+    if { $update_id ne -1 } {
+        after cancel $update_id
+        set update_id -1
+    }
+}
+
+proc handle_inotify { dir redraw } {
+    global have_inotify autoupdate
+    if { $have_inotify } {
+        if { $autoupdate } {
+            enable_inotify $dir $redraw
+        } else {
+            disable_inotify
+        }
+    }
+}
+
+handle_inotify $gitdir false
+
 set nullid "0000000000000000000000000000000000000000"
 set nullid2 "0000000000000000000000000000000000000001"
 set nullfile "/dev/null"
-- 
2.9.3