02e439cbc6a2c6aec238d1eb656ce26c594cbb3f
   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
  34field tooltip_wm     {} ; # Current tooltip toplevel, if open
  35field tooltip_timer  {} ; # Current timer event for our tooltip
  36field tooltip_commit {} ; # Commit in tooltip
  37field tooltip_text   {} ; # Text in current tooltip
  38
  39variable active_color #98e1a0
  40variable group_colors {
  41        #cbcbcb
  42        #e1e1e1
  43}
  44
  45constructor new {i_commit i_path} {
  46        variable active_color
  47        global cursor_ptr
  48
  49        set commit $i_commit
  50        set path   $i_path
  51
  52        make_toplevel top w
  53        wm title $top "[appname] ([reponame]): File Viewer"
  54        set status "Loading $commit:$path..."
  55
  56        label $w.path -text "$commit:$path" \
  57                -anchor w \
  58                -justify left \
  59                -borderwidth 1 \
  60                -relief sunken \
  61                -font font_uibold
  62        pack $w.path -side top -fill x
  63
  64        frame $w.out
  65        set w_load $w.out.loaded_t
  66        text $w_load \
  67                -background white -borderwidth 0 \
  68                -state disabled \
  69                -wrap none \
  70                -height 40 \
  71                -width 1 \
  72                -font font_diff
  73        $w_load tag conf annotated -background grey
  74
  75        set w_line $w.out.linenumber_t
  76        text $w_line \
  77                -background white -borderwidth 0 \
  78                -state disabled \
  79                -wrap none \
  80                -height 40 \
  81                -width 5 \
  82                -font font_diff
  83        $w_line tag conf linenumber -justify right
  84
  85        set w_cgrp $w.out.commit_t
  86        text $w_cgrp \
  87                -background white -borderwidth 0 \
  88                -state disabled \
  89                -wrap none \
  90                -height 40 \
  91                -width 4 \
  92                -font font_diff
  93
  94        set w_file $w.out.file_t
  95        text $w_file \
  96                -background white -borderwidth 0 \
  97                -state disabled \
  98                -wrap none \
  99                -height 40 \
 100                -width 80 \
 101                -xscrollcommand [list $w.out.sbx set] \
 102                -font font_diff
 103
 104        scrollbar $w.out.sbx -orient h -command [list $w_file xview]
 105        scrollbar $w.out.sby -orient v \
 106                -command [list scrollbar2many [list \
 107                $w_load \
 108                $w_line \
 109                $w_cgrp \
 110                $w_file \
 111                ] yview]
 112        grid \
 113                $w_cgrp \
 114                $w_line \
 115                $w_load \
 116                $w_file \
 117                $w.out.sby \
 118                -sticky nsew
 119        grid conf $w.out.sbx -column 3 -sticky we
 120        grid columnconfigure $w.out 3 -weight 1
 121        grid rowconfigure $w.out 0 -weight 1
 122        pack $w.out -fill both -expand 1
 123
 124        label $w.status \
 125                -textvariable @status \
 126                -anchor w \
 127                -justify left \
 128                -borderwidth 1 \
 129                -relief sunken
 130        pack $w.status -side bottom -fill x
 131
 132        frame $w.cm
 133        set w_cmit $w.cm.t
 134        text $w_cmit \
 135                -background white -borderwidth 0 \
 136                -state disabled \
 137                -wrap none \
 138                -height 10 \
 139                -width 80 \
 140                -xscrollcommand [list $w.cm.sbx set] \
 141                -yscrollcommand [list $w.cm.sby set] \
 142                -font font_diff
 143        $w_cmit tag conf header_key \
 144                -tabs {3c} \
 145                -background $active_color \
 146                -font font_uibold
 147        $w_cmit tag conf header_val \
 148                -background $active_color \
 149                -font font_ui
 150        $w_cmit tag raise sel
 151        scrollbar $w.cm.sbx -orient h -command [list $w_cmit xview]
 152        scrollbar $w.cm.sby -orient v -command [list $w_cmit yview]
 153        pack $w.cm.sby -side right -fill y
 154        pack $w.cm.sbx -side bottom -fill x
 155        pack $w_cmit -expand 1 -fill both
 156        pack $w.cm -side bottom -fill x
 157
 158        menu $w.ctxm -tearoff 0
 159        $w.ctxm add command \
 160                -label "Copy Commit" \
 161                -command [cb _copycommit]
 162
 163        foreach i [list \
 164                $w_cgrp \
 165                $w_load \
 166                $w_line \
 167                $w_file] {
 168                $i conf -cursor $cursor_ptr
 169                $i conf -yscrollcommand \
 170                        [list many2scrollbar [list \
 171                        $w_cgrp \
 172                        $w_load \
 173                        $w_line \
 174                        $w_file \
 175                        ] yview $w.out.sby]
 176                bind $i <Button-1> "
 177                        [cb _hide_tooltip]
 178                        [cb _click $i @%x,%y]
 179                        focus $i
 180                "
 181                bind $i <Any-Motion>  [cb _show_tooltip $i @%x,%y]
 182                bind $i <Any-Enter>   [cb _hide_tooltip]
 183                bind $i <Any-Leave>   [cb _hide_tooltip]
 184                bind_button3 $i "
 185                        [cb _hide_tooltip]
 186                        set cursorX %x
 187                        set cursorY %y
 188                        set cursorW %W
 189                        tk_popup $w.ctxm %X %Y
 190                "
 191        }
 192
 193        foreach i [list \
 194                $w_cgrp \
 195                $w_load \
 196                $w_line \
 197                $w_file \
 198                $w_cmit] {
 199                bind $i <Key-Up>        {catch {%W yview scroll -1 units};break}
 200                bind $i <Key-Down>      {catch {%W yview scroll  1 units};break}
 201                bind $i <Key-Left>      {catch {%W xview scroll -1 units};break}
 202                bind $i <Key-Right>     {catch {%W xview scroll  1 units};break}
 203                bind $i <Key-k>         {catch {%W yview scroll -1 units};break}
 204                bind $i <Key-j>         {catch {%W yview scroll  1 units};break}
 205                bind $i <Key-h>         {catch {%W xview scroll -1 units};break}
 206                bind $i <Key-l>         {catch {%W xview scroll  1 units};break}
 207                bind $i <Control-Key-b> {catch {%W yview scroll -1 pages};break}
 208                bind $i <Control-Key-f> {catch {%W yview scroll  1 pages};break}
 209        }
 210
 211        bind $w_cmit <Button-1> [list focus $w_cmit]
 212        bind $top <Visibility> [list focus $top]
 213        bind $top <Destroy> [list delete_this $this]
 214
 215        if {$commit eq {}} {
 216                set fd [open $path r]
 217        } else {
 218                set cmd [list git cat-file blob "$commit:$path"]
 219                set fd [open "| $cmd" r]
 220        }
 221        fconfigure $fd -blocking 0 -translation lf -encoding binary
 222        fileevent $fd readable [cb _read_file $fd]
 223}
 224
 225method _read_file {fd} {
 226        $w_load conf -state normal
 227        $w_cgrp conf -state normal
 228        $w_line conf -state normal
 229        $w_file conf -state normal
 230        while {[gets $fd line] >= 0} {
 231                regsub "\r\$" $line {} line
 232                incr total_lines
 233
 234                if {$total_lines > 1} {
 235                        $w_load insert end "\n"
 236                        $w_cgrp insert end "\n"
 237                        $w_line insert end "\n"
 238                        $w_file insert end "\n"
 239                }
 240
 241                $w_line insert end "$total_lines" linenumber
 242                $w_file insert end "$line"
 243        }
 244        $w_load conf -state disabled
 245        $w_cgrp conf -state disabled
 246        $w_line conf -state disabled
 247        $w_file conf -state disabled
 248
 249        if {[eof $fd]} {
 250                close $fd
 251                _status $this
 252                set cmd [list git blame -M -C --incremental]
 253                if {$commit eq {}} {
 254                        lappend cmd --contents $path
 255                } else {
 256                        lappend cmd $commit
 257                }
 258                lappend cmd -- $path
 259                set fd [open "| $cmd" r]
 260                fconfigure $fd -blocking 0 -translation lf -encoding binary
 261                fileevent $fd readable [cb _read_blame $fd]
 262        }
 263} ifdeleted { catch {close $fd} }
 264
 265method _read_blame {fd} {
 266        variable group_colors
 267
 268        $w_cgrp conf -state normal
 269        while {[gets $fd line] >= 0} {
 270                if {[regexp {^([a-z0-9]{40}) (\d+) (\d+) (\d+)$} $line line \
 271                        cmit original_line final_line line_count]} {
 272                        set r_commit     $cmit
 273                        set r_orig_line  $original_line
 274                        set r_final_line $final_line
 275                        set r_line_count $line_count
 276
 277                        if {[catch {set g $order($cmit)}]} {
 278                                set bg [lindex $group_colors 0]
 279                                set group_colors [lrange $group_colors 1 end]
 280                                lappend group_colors $bg
 281
 282                                $w_cgrp tag conf g$cmit -background $bg
 283                                $w_line tag conf g$cmit -background $bg
 284                                $w_file tag conf g$cmit -background $bg
 285
 286                                set order($cmit) $commit_count
 287                                incr commit_count
 288                                lappend commit_list $cmit
 289                        }
 290                } elseif {[string match {filename *} $line]} {
 291                        set file [string range $line 9 end]
 292                        set n    $r_line_count
 293                        set lno  $r_final_line
 294                        set cmit $r_commit
 295
 296                        if {[regexp {^0{40}$} $cmit]} {
 297                                set commit_abbr work
 298                        } else {
 299                                set commit_abbr [string range $cmit 0 4]
 300                        }
 301
 302                        set author_abbr {}
 303                        set a_name {}
 304                        catch {set a_name $header($cmit,author)}
 305                        while {$a_name ne {}} {
 306                                if {![regexp {^([[:upper:]])} $a_name _a]} break
 307                                append author_abbr $_a
 308                                unset _a
 309                                if {![regsub \
 310                                        {^[[:upper:]][^\s]*\s+} \
 311                                        $a_name {} a_name ]} break
 312                        }
 313                        if {$author_abbr eq {}} {
 314                                set author_abbr { |}
 315                        } else {
 316                                set author_abbr [string range $author_abbr 0 3]
 317                                while {[string length $author_abbr] < 4} {
 318                                        set author_abbr " $author_abbr"
 319                                }
 320                        }
 321                        unset a_name
 322
 323                        set first_lno $lno
 324                        while {
 325                        ![catch {set ncmit $line_commit([expr {$first_lno - 1}])}]
 326                        && $ncmit eq $cmit
 327                        } {
 328                                incr first_lno -1
 329                        }
 330
 331                        while {$n > 0} {
 332                                set lno_e "$lno.0 lineend + 1c"
 333                                if {[catch {set g g$line_commit($lno)}]} {
 334                                        $w_load tag add annotated $lno.0 $lno_e
 335                                } else {
 336                                        $w_cgrp tag remove g$g $lno.0 $lno_e
 337                                        $w_line tag remove g$g $lno.0 $lno_e
 338                                        $w_file tag remove g$g $lno.0 $lno_e
 339
 340                                        $w_cgrp tag remove a$g $lno.0 $lno_e
 341                                        $w_line tag remove a$g $lno.0 $lno_e
 342                                        $w_file tag remove a$g $lno.0 $lno_e
 343                                }
 344
 345                                set line_commit($lno) $cmit
 346                                set line_file($lno)   $file
 347
 348                                $w_cgrp delete $lno.0 "$lno.0 lineend"
 349                                if {$lno == $first_lno} {
 350                                        $w_cgrp insert $lno.0 $commit_abbr
 351                                } elseif {$lno == [expr {$first_lno + 1}]} {
 352                                        $w_cgrp insert $lno.0 $author_abbr
 353                                } else {
 354                                        $w_cgrp insert $lno.0 { |}
 355                                }
 356
 357                                $w_cgrp tag add g$cmit $lno.0 $lno_e
 358                                $w_line tag add g$cmit $lno.0 $lno_e
 359                                $w_file tag add g$cmit $lno.0 $lno_e
 360
 361                                $w_cgrp tag add a$cmit $lno.0 $lno_e
 362                                $w_line tag add a$cmit $lno.0 $lno_e
 363                                $w_file tag add a$cmit $lno.0 $lno_e
 364
 365                                if {$highlight_line == -1} {
 366                                        if {[lindex [$w_file yview] 0] == 0} {
 367                                                $w_file see $lno.0
 368                                                _showcommit $this $lno
 369                                        }
 370                                } elseif {$highlight_line == $lno} {
 371                                        _showcommit $this $lno
 372                                }
 373
 374                                incr n -1
 375                                incr lno
 376                                incr blame_lines
 377                        }
 378
 379                        while {![catch {set ncmit $line_commit($lno)}]
 380                                && $ncmit eq $cmit} {
 381                                $w_cgrp delete $lno.0 "$lno.0 lineend"
 382
 383                                if {$lno == $first_lno} {
 384                                        $w_cgrp insert $lno.0 $commit_abbr
 385                                } elseif {$lno == [expr {$first_lno + 1}]} {
 386                                        $w_cgrp insert $lno.0 $author_abbr
 387                                } else {
 388                                        $w_cgrp insert $lno.0 { |}
 389                                }
 390                                incr lno
 391                        }
 392
 393                } elseif {[regexp {^([a-z-]+) (.*)$} $line line key data]} {
 394                        set header($r_commit,$key) $data
 395                }
 396        }
 397        $w_cgrp conf -state disabled
 398
 399        if {[eof $fd]} {
 400                close $fd
 401                set status {Annotation complete.}
 402        } else {
 403                _status $this
 404        }
 405} ifdeleted { catch {close $fd} }
 406
 407method _status {} {
 408        set have  $blame_lines
 409        set total $total_lines
 410        set pdone 0
 411        if {$total} {set pdone [expr {100 * $have / $total}]}
 412
 413        set status [format \
 414                "Loading annotations... %i of %i lines annotated (%2i%%)" \
 415                $have $total $pdone]
 416}
 417
 418method _click {cur_w pos} {
 419        set lno [lindex [split [$cur_w index $pos] .] 0]
 420        if {$lno eq {}} return
 421        _showcommit $this $lno
 422}
 423
 424method _showcommit {lno} {
 425        global repo_config
 426        variable active_color
 427
 428        if {$highlight_commit ne {}} {
 429                set cmit $highlight_commit
 430                $w_cgrp tag conf a$cmit -background {}
 431                $w_line tag conf a$cmit -background {}
 432                $w_file tag conf a$cmit -background {}
 433        }
 434
 435        $w_cmit conf -state normal
 436        $w_cmit delete 0.0 end
 437        if {[catch {set cmit $line_commit($lno)}]} {
 438                set cmit {}
 439                $w_cmit insert end "Loading annotation..."
 440        } else {
 441                $w_cgrp tag conf a$cmit -background $active_color
 442                $w_line tag conf a$cmit -background $active_color
 443                $w_file tag conf a$cmit -background $active_color
 444
 445                set author_name {}
 446                set author_email {}
 447                set author_time {}
 448                catch {set author_name $header($cmit,author)}
 449                catch {set author_email $header($cmit,author-mail)}
 450                catch {set author_time [clock format \
 451                        $header($cmit,author-time) \
 452                        -format {%Y-%m-%d %H:%M:%S}
 453                ]}
 454
 455                set committer_name {}
 456                set committer_email {}
 457                set committer_time {}
 458                catch {set committer_name $header($cmit,committer)}
 459                catch {set committer_email $header($cmit,committer-mail)}
 460                catch {set committer_time [clock format \
 461                        $header($cmit,committer-time) \
 462                        -format {%Y-%m-%d %H:%M:%S}
 463                ]}
 464
 465                if {[catch {set msg $header($cmit,message)}]} {
 466                        set msg {}
 467                        catch {
 468                                set fd [open "| git cat-file commit $cmit" r]
 469                                fconfigure $fd -encoding binary -translation lf
 470                                if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
 471                                        set enc utf-8
 472                                }
 473                                while {[gets $fd line] > 0} {
 474                                        if {[string match {encoding *} $line]} {
 475                                                set enc [string tolower [string range $line 9 end]]
 476                                        }
 477                                }
 478                                set msg [encoding convertfrom $enc [read $fd]]
 479                                set msg [string trim $msg]
 480                                close $fd
 481
 482                                set author_name [encoding convertfrom $enc $author_name]
 483                                set committer_name [encoding convertfrom $enc $committer_name]
 484
 485                                set header($cmit,author) $author_name
 486                                set header($cmit,committer) $committer_name
 487                        }
 488                        set header($cmit,message) $msg
 489                }
 490
 491                $w_cmit insert end "commit $cmit\n" header_key
 492                $w_cmit insert end "Author:\t" header_key
 493                $w_cmit insert end "$author_name $author_email" header_val
 494                $w_cmit insert end "$author_time\n" header_val
 495
 496                $w_cmit insert end "Committer:\t" header_key
 497                $w_cmit insert end "$committer_name $committer_email" header_val
 498                $w_cmit insert end "$committer_time\n" header_val
 499
 500                if {$line_file($lno) ne $path} {
 501                        $w_cmit insert end "Original File:\t" header_key
 502                        $w_cmit insert end "[escape_path $line_file($lno)]\n" header_val
 503                }
 504
 505                $w_cmit insert end "\n$msg"
 506        }
 507        $w_cmit conf -state disabled
 508
 509        set highlight_line $lno
 510        set highlight_commit $cmit
 511
 512        if {$highlight_commit eq $tooltip_commit} {
 513                _hide_tooltip $this
 514        }
 515}
 516
 517method _copycommit {} {
 518        set pos @$::cursorX,$::cursorY
 519        set lno [lindex [split [$::cursorW index $pos] .] 0]
 520        if {![catch {set commit $line_commit($lno)}]} {
 521                clipboard clear
 522                clipboard append \
 523                        -format STRING \
 524                        -type STRING \
 525                        -- $commit
 526        }
 527}
 528
 529method _show_tooltip {cur_w pos} {
 530        set lno [lindex [split [$cur_w index $pos] .] 0]
 531        if {[catch {set cmit $line_commit($lno)}]} {
 532                _hide_tooltip $this
 533                return
 534        }
 535
 536        if {$cmit eq $highlight_commit} {
 537                _hide_tooltip $this
 538                return
 539        }
 540
 541        if {$cmit eq $tooltip_commit} {
 542                _position_tooltip $this
 543        } elseif {$tooltip_wm ne {}} {
 544                _open_tooltip $this $cur_w
 545        } elseif {$tooltip_timer eq {}} {
 546                set tooltip_timer [after 1000 [cb _open_tooltip $cur_w]]
 547        }
 548}
 549
 550method _open_tooltip {cur_w} {
 551        set tooltip_timer {}
 552        set pos_x [winfo pointerx $cur_w]
 553        set pos_y [winfo pointery $cur_w]
 554        if {[winfo containing $pos_x $pos_y] ne $cur_w} {
 555                _hide_tooltip $this
 556                return
 557        }
 558
 559        set pos @[join [list \
 560                [expr {$pos_x - [winfo rootx $cur_w]}] \
 561                [expr {$pos_y - [winfo rooty $cur_w]}]] ,]
 562        set lno [lindex [split [$cur_w index $pos] .] 0]
 563        set cmit $line_commit($lno)
 564
 565        set author_name {}
 566        set author_email {}
 567        set author_time {}
 568        catch {set author_name $header($cmit,author)}
 569        catch {set author_email $header($cmit,author-mail)}
 570        catch {set author_time [clock format \
 571                $header($cmit,author-time) \
 572                -format {%Y-%m-%d %H:%M:%S}
 573        ]}
 574
 575        set committer_name {}
 576        set committer_email {}
 577        set committer_time {}
 578        catch {set committer_name $header($cmit,committer)}
 579        catch {set committer_email $header($cmit,committer-mail)}
 580        catch {set committer_time [clock format \
 581                $header($cmit,committer-time) \
 582                -format {%Y-%m-%d %H:%M:%S}
 583        ]}
 584
 585        set summary {}
 586        catch {set summary $header($cmit,summary)}
 587
 588        set tooltip_commit $cmit
 589        set tooltip_text "commit $cmit
 590$author_name $author_email  $author_time
 591$summary"
 592
 593        if {$tooltip_wm ne "$cur_w.tooltip"} {
 594                _hide_tooltip $this
 595
 596                set tooltip_wm [toplevel $cur_w.tooltip -borderwidth 1]
 597                wm overrideredirect $tooltip_wm 1
 598                wm transient $tooltip_wm [winfo toplevel $cur_w]
 599                pack [label $tooltip_wm.label \
 600                        -background lightyellow \
 601                        -foreground black \
 602                        -textvariable @tooltip_text \
 603                        -justify left]
 604        }
 605        _position_tooltip $this
 606}
 607
 608method _position_tooltip {} {
 609        set req_w [winfo reqwidth  $tooltip_wm.label]
 610        set req_h [winfo reqheight $tooltip_wm.label]
 611        set pos_x [expr {[winfo pointerx .] +  5}]
 612        set pos_y [expr {[winfo pointery .] + 10}]
 613
 614        set g "${req_w}x${req_h}"
 615        if {$pos_x >= 0} {append g +}
 616        append g $pos_x
 617        if {$pos_y >= 0} {append g +}
 618        append g $pos_y
 619
 620        wm geometry $tooltip_wm $g
 621        raise $tooltip_wm
 622}
 623
 624method _hide_tooltip {} {
 625        if {$tooltip_wm ne {}} {
 626                destroy $tooltip_wm
 627                set tooltip_wm {}
 628                set tooltip_commit {}
 629        }
 630        if {$tooltip_timer ne {}} {
 631                after cancel $tooltip_timer
 632                set tooltip_timer {}
 633        }
 634}
 635
 636}