lib / blame.tclon commit git-gui: Refactor into multiple files to save my sanity (f522c9b)
   1# git-gui blame viewer
   2# Copyright (C) 2006, 2007 Shawn Pearce
   3
   4proc show_blame {commit path} {
   5        global next_browser_id blame_status blame_data
   6
   7        if {[winfo ismapped .]} {
   8                set w .browser[incr next_browser_id]
   9                set tl $w
  10                toplevel $w
  11        } else {
  12                set w {}
  13                set tl .
  14        }
  15        set blame_status($w) {Loading current file content...}
  16
  17        label $w.path -text "$commit:$path" \
  18                -anchor w \
  19                -justify left \
  20                -borderwidth 1 \
  21                -relief sunken \
  22                -font font_uibold
  23        pack $w.path -side top -fill x
  24
  25        frame $w.out
  26        text $w.out.loaded_t \
  27                -background white -borderwidth 0 \
  28                -state disabled \
  29                -wrap none \
  30                -height 40 \
  31                -width 1 \
  32                -font font_diff
  33        $w.out.loaded_t tag conf annotated -background grey
  34
  35        text $w.out.linenumber_t \
  36                -background white -borderwidth 0 \
  37                -state disabled \
  38                -wrap none \
  39                -height 40 \
  40                -width 5 \
  41                -font font_diff
  42        $w.out.linenumber_t tag conf linenumber -justify right
  43
  44        text $w.out.file_t \
  45                -background white -borderwidth 0 \
  46                -state disabled \
  47                -wrap none \
  48                -height 40 \
  49                -width 80 \
  50                -xscrollcommand [list $w.out.sbx set] \
  51                -font font_diff
  52
  53        scrollbar $w.out.sbx -orient h -command [list $w.out.file_t xview]
  54        scrollbar $w.out.sby -orient v \
  55                -command [list scrollbar2many [list \
  56                $w.out.loaded_t \
  57                $w.out.linenumber_t \
  58                $w.out.file_t \
  59                ] yview]
  60        grid \
  61                $w.out.linenumber_t \
  62                $w.out.loaded_t \
  63                $w.out.file_t \
  64                $w.out.sby \
  65                -sticky nsew
  66        grid conf $w.out.sbx -column 2 -sticky we
  67        grid columnconfigure $w.out 2 -weight 1
  68        grid rowconfigure $w.out 0 -weight 1
  69        pack $w.out -fill both -expand 1
  70
  71        label $w.status -textvariable blame_status($w) \
  72                -anchor w \
  73                -justify left \
  74                -borderwidth 1 \
  75                -relief sunken
  76        pack $w.status -side bottom -fill x
  77
  78        frame $w.cm
  79        text $w.cm.t \
  80                -background white -borderwidth 0 \
  81                -state disabled \
  82                -wrap none \
  83                -height 10 \
  84                -width 80 \
  85                -xscrollcommand [list $w.cm.sbx set] \
  86                -yscrollcommand [list $w.cm.sby set] \
  87                -font font_diff
  88        scrollbar $w.cm.sbx -orient h -command [list $w.cm.t xview]
  89        scrollbar $w.cm.sby -orient v -command [list $w.cm.t yview]
  90        pack $w.cm.sby -side right -fill y
  91        pack $w.cm.sbx -side bottom -fill x
  92        pack $w.cm.t -expand 1 -fill both
  93        pack $w.cm -side bottom -fill x
  94
  95        menu $w.ctxm -tearoff 0
  96        $w.ctxm add command -label "Copy Commit" \
  97                -command "blame_copycommit $w \$cursorW @\$cursorX,\$cursorY"
  98
  99        foreach i [list \
 100                $w.out.loaded_t \
 101                $w.out.linenumber_t \
 102                $w.out.file_t] {
 103                $i tag conf in_sel \
 104                        -background [$i cget -foreground] \
 105                        -foreground [$i cget -background]
 106                $i conf -yscrollcommand \
 107                        [list many2scrollbar [list \
 108                        $w.out.loaded_t \
 109                        $w.out.linenumber_t \
 110                        $w.out.file_t \
 111                        ] yview $w.out.sby]
 112                bind $i <Button-1> "
 113                        blame_click {$w} \\
 114                                $w.cm.t \\
 115                                $w.out.linenumber_t \\
 116                                $w.out.file_t \\
 117                                $i @%x,%y
 118                        focus $i
 119                "
 120                bind_button3 $i "
 121                        set cursorX %x
 122                        set cursorY %y
 123                        set cursorW %W
 124                        tk_popup $w.ctxm %X %Y
 125                "
 126        }
 127
 128        bind $w.cm.t <Button-1> "focus $w.cm.t"
 129        bind $tl <Visibility> "focus $tl"
 130        bind $tl <Destroy> "
 131                array unset blame_status {$w}
 132                array unset blame_data $w,*
 133        "
 134        wm title $tl "[appname] ([reponame]): File Viewer"
 135
 136        set blame_data($w,commit_count) 0
 137        set blame_data($w,commit_list) {}
 138        set blame_data($w,total_lines) 0
 139        set blame_data($w,blame_lines) 0
 140        set blame_data($w,highlight_commit) {}
 141        set blame_data($w,highlight_line) -1
 142
 143        set cmd [list git cat-file blob "$commit:$path"]
 144        set fd [open "| $cmd" r]
 145        fconfigure $fd -blocking 0 -translation lf -encoding binary
 146        fileevent $fd readable [list read_blame_catfile \
 147                $fd $w $commit $path \
 148                $w.cm.t $w.out.loaded_t $w.out.linenumber_t $w.out.file_t]
 149}
 150
 151proc read_blame_catfile {fd w commit path w_cmit w_load w_line w_file} {
 152        global blame_status blame_data
 153
 154        if {![winfo exists $w_file]} {
 155                catch {close $fd}
 156                return
 157        }
 158
 159        set n $blame_data($w,total_lines)
 160        $w_load conf -state normal
 161        $w_line conf -state normal
 162        $w_file conf -state normal
 163        while {[gets $fd line] >= 0} {
 164                regsub "\r\$" $line {} line
 165                incr n
 166                $w_load insert end "\n"
 167                $w_line insert end "$n\n" linenumber
 168                $w_file insert end "$line\n"
 169        }
 170        $w_load conf -state disabled
 171        $w_line conf -state disabled
 172        $w_file conf -state disabled
 173        set blame_data($w,total_lines) $n
 174
 175        if {[eof $fd]} {
 176                close $fd
 177                blame_incremental_status $w
 178                set cmd [list git blame -M -C --incremental]
 179                lappend cmd $commit -- $path
 180                set fd [open "| $cmd" r]
 181                fconfigure $fd -blocking 0 -translation lf -encoding binary
 182                fileevent $fd readable [list read_blame_incremental $fd $w \
 183                        $w_load $w_cmit $w_line $w_file]
 184        }
 185}
 186
 187proc read_blame_incremental {fd w w_load w_cmit w_line w_file} {
 188        global blame_status blame_data
 189
 190        if {![winfo exists $w_file]} {
 191                catch {close $fd}
 192                return
 193        }
 194
 195        while {[gets $fd line] >= 0} {
 196                if {[regexp {^([a-z0-9]{40}) (\d+) (\d+) (\d+)$} $line line \
 197                        cmit original_line final_line line_count]} {
 198                        set blame_data($w,commit) $cmit
 199                        set blame_data($w,original_line) $original_line
 200                        set blame_data($w,final_line) $final_line
 201                        set blame_data($w,line_count) $line_count
 202
 203                        if {[catch {set g $blame_data($w,$cmit,order)}]} {
 204                                $w_line tag conf g$cmit
 205                                $w_file tag conf g$cmit
 206                                $w_line tag raise in_sel
 207                                $w_file tag raise in_sel
 208                                $w_file tag raise sel
 209                                set blame_data($w,$cmit,order) $blame_data($w,commit_count)
 210                                incr blame_data($w,commit_count)
 211                                lappend blame_data($w,commit_list) $cmit
 212                        }
 213                } elseif {[string match {filename *} $line]} {
 214                        set file [string range $line 9 end]
 215                        set n $blame_data($w,line_count)
 216                        set lno $blame_data($w,final_line)
 217                        set cmit $blame_data($w,commit)
 218
 219                        while {$n > 0} {
 220                                if {[catch {set g g$blame_data($w,line$lno,commit)}]} {
 221                                        $w_load tag add annotated $lno.0 "$lno.0 lineend + 1c"
 222                                } else {
 223                                        $w_line tag remove g$g $lno.0 "$lno.0 lineend + 1c"
 224                                        $w_file tag remove g$g $lno.0 "$lno.0 lineend + 1c"
 225                                }
 226
 227                                set blame_data($w,line$lno,commit) $cmit
 228                                set blame_data($w,line$lno,file) $file
 229                                $w_line tag add g$cmit $lno.0 "$lno.0 lineend + 1c"
 230                                $w_file tag add g$cmit $lno.0 "$lno.0 lineend + 1c"
 231
 232                                if {$blame_data($w,highlight_line) == -1} {
 233                                        if {[lindex [$w_file yview] 0] == 0} {
 234                                                $w_file see $lno.0
 235                                                blame_showcommit $w $w_cmit $w_line $w_file $lno
 236                                        }
 237                                } elseif {$blame_data($w,highlight_line) == $lno} {
 238                                        blame_showcommit $w $w_cmit $w_line $w_file $lno
 239                                }
 240
 241                                incr n -1
 242                                incr lno
 243                                incr blame_data($w,blame_lines)
 244                        }
 245
 246                        set hc $blame_data($w,highlight_commit)
 247                        if {$hc ne {}
 248                                && [expr {$blame_data($w,$hc,order) + 1}]
 249                                        == $blame_data($w,$cmit,order)} {
 250                                blame_showcommit $w $w_cmit $w_line $w_file \
 251                                        $blame_data($w,highlight_line)
 252                        }
 253                } elseif {[regexp {^([a-z-]+) (.*)$} $line line header data]} {
 254                        set blame_data($w,$blame_data($w,commit),$header) $data
 255                }
 256        }
 257
 258        if {[eof $fd]} {
 259                close $fd
 260                set blame_status($w) {Annotation complete.}
 261        } else {
 262                blame_incremental_status $w
 263        }
 264}
 265
 266proc blame_incremental_status {w} {
 267        global blame_status blame_data
 268
 269        set have  $blame_data($w,blame_lines)
 270        set total $blame_data($w,total_lines)
 271        set pdone 0
 272        if {$total} {set pdone [expr {100 * $have / $total}]}
 273
 274        set blame_status($w) [format \
 275                "Loading annotations... %i of %i lines annotated (%2i%%)" \
 276                $have $total $pdone]
 277}
 278
 279proc blame_click {w w_cmit w_line w_file cur_w pos} {
 280        set lno [lindex [split [$cur_w index $pos] .] 0]
 281        if {$lno eq {}} return
 282
 283        $w_line tag remove in_sel 0.0 end
 284        $w_file tag remove in_sel 0.0 end
 285        $w_line tag add in_sel $lno.0 "$lno.0 + 1 line"
 286        $w_file tag add in_sel $lno.0 "$lno.0 + 1 line"
 287
 288        blame_showcommit $w $w_cmit $w_line $w_file $lno
 289}
 290
 291set blame_colors {
 292        #ff4040
 293        #ff40ff
 294        #4040ff
 295}
 296
 297proc blame_showcommit {w w_cmit w_line w_file lno} {
 298        global blame_colors blame_data repo_config
 299
 300        set cmit $blame_data($w,highlight_commit)
 301        if {$cmit ne {}} {
 302                set idx $blame_data($w,$cmit,order)
 303                set i 0
 304                foreach c $blame_colors {
 305                        set h [lindex $blame_data($w,commit_list) [expr {$idx - 1 + $i}]]
 306                        $w_line tag conf g$h -background white
 307                        $w_file tag conf g$h -background white
 308                        incr i
 309                }
 310        }
 311
 312        $w_cmit conf -state normal
 313        $w_cmit delete 0.0 end
 314        if {[catch {set cmit $blame_data($w,line$lno,commit)}]} {
 315                set cmit {}
 316                $w_cmit insert end "Loading annotation..."
 317        } else {
 318                set idx $blame_data($w,$cmit,order)
 319                set i 0
 320                foreach c $blame_colors {
 321                        set h [lindex $blame_data($w,commit_list) [expr {$idx - 1 + $i}]]
 322                        $w_line tag conf g$h -background $c
 323                        $w_file tag conf g$h -background $c
 324                        incr i
 325                }
 326
 327                set author_name {}
 328                set author_email {}
 329                set author_time {}
 330                catch {set author_name $blame_data($w,$cmit,author)}
 331                catch {set author_email $blame_data($w,$cmit,author-mail)}
 332                catch {set author_time [clock format $blame_data($w,$cmit,author-time)]}
 333
 334                set committer_name {}
 335                set committer_email {}
 336                set committer_time {}
 337                catch {set committer_name $blame_data($w,$cmit,committer)}
 338                catch {set committer_email $blame_data($w,$cmit,committer-mail)}
 339                catch {set committer_time [clock format $blame_data($w,$cmit,committer-time)]}
 340
 341                if {[catch {set msg $blame_data($w,$cmit,message)}]} {
 342                        set msg {}
 343                        catch {
 344                                set fd [open "| git cat-file commit $cmit" r]
 345                                fconfigure $fd -encoding binary -translation lf
 346                                if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
 347                                        set enc utf-8
 348                                }
 349                                while {[gets $fd line] > 0} {
 350                                        if {[string match {encoding *} $line]} {
 351                                                set enc [string tolower [string range $line 9 end]]
 352                                        }
 353                                }
 354                                set msg [encoding convertfrom $enc [read $fd]]
 355                                set msg [string trim $msg]
 356                                close $fd
 357
 358                                set author_name [encoding convertfrom $enc $author_name]
 359                                set committer_name [encoding convertfrom $enc $committer_name]
 360
 361                                set blame_data($w,$cmit,author) $author_name
 362                                set blame_data($w,$cmit,committer) $committer_name
 363                        }
 364                        set blame_data($w,$cmit,message) $msg
 365                }
 366
 367                $w_cmit insert end "commit $cmit\n"
 368                $w_cmit insert end "Author: $author_name $author_email $author_time\n"
 369                $w_cmit insert end "Committer: $committer_name $committer_email $committer_time\n"
 370                $w_cmit insert end "Original File: [escape_path $blame_data($w,line$lno,file)]\n"
 371                $w_cmit insert end "\n"
 372                $w_cmit insert end $msg
 373        }
 374        $w_cmit conf -state disabled
 375
 376        set blame_data($w,highlight_line) $lno
 377        set blame_data($w,highlight_commit) $cmit
 378}
 379
 380proc blame_copycommit {w i pos} {
 381        global blame_data
 382        set lno [lindex [split [$i index $pos] .] 0]
 383        if {![catch {set commit $blame_data($w,line$lno,commit)}]} {
 384                clipboard clear
 385                clipboard append \
 386                        -format STRING \
 387                        -type STRING \
 388                        -- $commit
 389        }
 390}