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