221313c76849c57875525b8ed1acc6f56c83bd7d
   1# git-gui blame viewer
   2# Copyright (C) 2006, 2007 Shawn Pearce
   3
   4class blame {
   5
   6image create photo ::blame::img_back_arrow -data {R0lGODlhGAAYAIUAAPwCBEzKXFTSZIz+nGzmhGzqfGTidIT+nEzGXHTqhGzmfGzifFzadETCVES+VARWDFzWbHzyjAReDGTadFTOZDSyRDyyTCymPARaFGTedFzSbDy2TCyqRCyqPARaDAyCHES6VDy6VCyiPAR6HCSeNByWLARyFARiDARqFGTifARiFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAYABgAAAajQIBwSCwaj8ikcsk0BppJwRPqHEypQwHBis0WDAdEFyBIKBaMAKLBdjQeSkFBYTBAIvgEoS6JmhUTEwIUDQ4VFhcMGEhyCgoZExoUaxsWHB0THkgfAXUGAhoBDSAVFR0XBnCbDRmgog0hpSIiDJpJIyEQhBUcJCIlwA22SSYVogknEg8eD82qSigdDSknY0IqJQXPYxIl1dZCGNvWw+Dm510GQQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7}
   7
   8# Persistant data (survives loads)
   9#
  10field history {}; # viewer history: {commit path}
  11field header    ; # array commit,key -> header field
  12
  13# Tk UI control paths
  14#
  15field w          ; # top window in this viewer
  16field w_back     ; # our back button
  17field w_path     ; # label showing the current file path
  18field w_columns  ; # list of all column widgets in the viewer
  19field w_line     ; # text column: all line numbers
  20field w_amov     ; # text column: annotations + move tracking
  21field w_asim     ; # text column: annotations (simple computation)
  22field w_file     ; # text column: actual file data
  23field w_cviewer  ; # pane showing commit message
  24field status     ; # status mega-widget instance
  25field old_height ; # last known height of $w.file_pane
  26
  27# Tk UI colors
  28#
  29variable active_color #c0edc5
  30variable group_colors {
  31        #d6d6d6
  32        #e1e1e1
  33        #ececec
  34}
  35
  36# Current blame data; cleared/reset on each load
  37#
  38field commit               ; # input commit to blame
  39field path                 ; # input filename to view in $commit
  40
  41field current_fd        {} ; # background process running
  42field highlight_line    -1 ; # current line selected
  43field highlight_column  {} ; # current commit column selected
  44field highlight_commit  {} ; # sha1 of commit selected
  45
  46field total_lines       0  ; # total length of file
  47field blame_lines       0  ; # number of lines computed
  48field amov_data            ; # list of {commit origfile origline}
  49field asim_data            ; # list of {commit origfile origline}
  50
  51field r_commit             ; # commit currently being parsed
  52field r_orig_line          ; # original line number
  53field r_final_line         ; # final line number
  54field r_line_count         ; # lines in this region
  55
  56field tooltip_wm        {} ; # Current tooltip toplevel, if open
  57field tooltip_t         {} ; # Text widget in $tooltip_wm
  58field tooltip_timer     {} ; # Current timer event for our tooltip
  59field tooltip_commit    {} ; # Commit(s) in tooltip
  60
  61constructor new {i_commit i_path i_jump} {
  62        global cursor_ptr
  63        variable active_color
  64        variable group_colors
  65
  66        set commit $i_commit
  67        set path   $i_path
  68
  69        make_toplevel top w
  70        wm title $top [append "[appname] ([reponame]): " [mc "File Viewer"]]
  71
  72        set font_w [font measure font_diff "0"]
  73
  74        frame $w.header -background gold
  75        label $w.header.commit_l \
  76                -text [mc "Commit:"] \
  77                -background gold \
  78                -foreground black \
  79                -anchor w \
  80                -justify left
  81        set w_back $w.header.commit_b
  82        label $w_back \
  83                -image ::blame::img_back_arrow \
  84                -borderwidth 0 \
  85                -relief flat \
  86                -state disabled \
  87                -background gold \
  88                -foreground black \
  89                -activebackground gold
  90        bind $w_back <Button-1> "
  91                if {\[$w_back cget -state\] eq {normal}} {
  92                        [cb _history_menu]
  93                }
  94                "
  95        label $w.header.commit \
  96                -textvariable @commit \
  97                -background gold \
  98                -foreground black \
  99                -anchor w \
 100                -justify left
 101        label $w.header.path_l \
 102                -text [mc "File:"] \
 103                -background gold \
 104                -foreground black \
 105                -anchor w \
 106                -justify left
 107        set w_path $w.header.path
 108        label $w_path \
 109                -background gold \
 110                -foreground black \
 111                -anchor w \
 112                -justify left
 113        pack $w.header.commit_l -side left
 114        pack $w_back -side left
 115        pack $w.header.commit -side left
 116        pack $w_path -fill x -side right
 117        pack $w.header.path_l -side right
 118
 119        panedwindow $w.file_pane -orient vertical -borderwidth 0 -sashwidth 3
 120        frame $w.file_pane.out -relief flat -borderwidth 1
 121        frame $w.file_pane.cm -relief sunken -borderwidth 1
 122        $w.file_pane add $w.file_pane.out \
 123                -sticky nsew \
 124                -minsize 100 \
 125                -height 100 \
 126                -width 100
 127        $w.file_pane add $w.file_pane.cm \
 128                -sticky nsew \
 129                -minsize 25 \
 130                -height 25 \
 131                -width 100
 132
 133        set w_line $w.file_pane.out.linenumber_t
 134        text $w_line \
 135                -takefocus 0 \
 136                -highlightthickness 0 \
 137                -padx 0 -pady 0 \
 138                -background white \
 139                -foreground black \
 140                -borderwidth 0 \
 141                -state disabled \
 142                -wrap none \
 143                -height 40 \
 144                -width 6 \
 145                -font font_diff
 146        $w_line tag conf linenumber -justify right -rmargin 5
 147
 148        set w_amov $w.file_pane.out.amove_t
 149        text $w_amov \
 150                -takefocus 0 \
 151                -highlightthickness 0 \
 152                -padx 0 -pady 0 \
 153                -background white \
 154                -foreground black \
 155                -borderwidth 0 \
 156                -state disabled \
 157                -wrap none \
 158                -height 40 \
 159                -width 5 \
 160                -font font_diff
 161        $w_amov tag conf author_abbr -justify right -rmargin 5
 162        $w_amov tag conf curr_commit
 163        $w_amov tag conf prior_commit -foreground blue -underline 1
 164        $w_amov tag bind prior_commit \
 165                <Button-1> \
 166                "[cb _load_commit $w_amov @amov_data @%x,%y];break"
 167
 168        set w_asim $w.file_pane.out.asimple_t
 169        text $w_asim \
 170                -takefocus 0 \
 171                -highlightthickness 0 \
 172                -padx 0 -pady 0 \
 173                -background white \
 174                -foreground black \
 175                -borderwidth 0 \
 176                -state disabled \
 177                -wrap none \
 178                -height 40 \
 179                -width 4 \
 180                -font font_diff
 181        $w_asim tag conf author_abbr -justify right
 182        $w_asim tag conf curr_commit
 183        $w_asim tag conf prior_commit -foreground blue -underline 1
 184        $w_asim tag bind prior_commit \
 185                <Button-1> \
 186                "[cb _load_commit $w_asim @asim_data @%x,%y];break"
 187
 188        set w_file $w.file_pane.out.file_t
 189        text $w_file \
 190                -takefocus 0 \
 191                -highlightthickness 0 \
 192                -padx 0 -pady 0 \
 193                -background white \
 194                -foreground black \
 195                -borderwidth 0 \
 196                -state disabled \
 197                -wrap none \
 198                -height 40 \
 199                -width 80 \
 200                -xscrollcommand [list $w.file_pane.out.sbx set] \
 201                -font font_diff
 202
 203        set w_columns [list $w_amov $w_asim $w_line $w_file]
 204
 205        scrollbar $w.file_pane.out.sbx \
 206                -orient h \
 207                -command [list $w_file xview]
 208        scrollbar $w.file_pane.out.sby \
 209                -orient v \
 210                -command [list scrollbar2many $w_columns yview]
 211        eval grid $w_columns $w.file_pane.out.sby -sticky nsew
 212        grid conf \
 213                $w.file_pane.out.sbx \
 214                -column [expr {[llength $w_columns] - 1}] \
 215                -sticky we
 216        grid columnconfigure \
 217                $w.file_pane.out \
 218                [expr {[llength $w_columns] - 1}] \
 219                -weight 1
 220        grid rowconfigure $w.file_pane.out 0 -weight 1
 221
 222        set w_cviewer $w.file_pane.cm.t
 223        text $w_cviewer \
 224                -background white \
 225                -foreground black \
 226                -borderwidth 0 \
 227                -state disabled \
 228                -wrap none \
 229                -height 10 \
 230                -width 80 \
 231                -xscrollcommand [list $w.file_pane.cm.sbx set] \
 232                -yscrollcommand [list $w.file_pane.cm.sby set] \
 233                -font font_diff
 234        $w_cviewer tag conf still_loading \
 235                -font font_uiitalic \
 236                -justify center
 237        $w_cviewer tag conf header_key \
 238                -tabs {3c} \
 239                -background $active_color \
 240                -font font_uibold
 241        $w_cviewer tag conf header_val \
 242                -background $active_color \
 243                -font font_ui
 244        $w_cviewer tag raise sel
 245        scrollbar $w.file_pane.cm.sbx \
 246                -orient h \
 247                -command [list $w_cviewer xview]
 248        scrollbar $w.file_pane.cm.sby \
 249                -orient v \
 250                -command [list $w_cviewer yview]
 251        pack $w.file_pane.cm.sby -side right -fill y
 252        pack $w.file_pane.cm.sbx -side bottom -fill x
 253        pack $w_cviewer -expand 1 -fill both
 254
 255        set status [::status_bar::new $w.status]
 256
 257        menu $w.ctxm -tearoff 0
 258        $w.ctxm add command \
 259                -label [mc "Copy Commit"] \
 260                -command [cb _copycommit]
 261        $w.ctxm add separator
 262        menu $w.ctxm.enc
 263        build_encoding_menu $w.ctxm.enc [cb _setencoding]
 264        $w.ctxm add cascade \
 265                -label [mc "Encoding"] \
 266                -menu $w.ctxm.enc
 267        $w.ctxm add command \
 268                -label [mc "Do Full Copy Detection"] \
 269                -command [cb _fullcopyblame]
 270        $w.ctxm add separator
 271        $w.ctxm add command \
 272                -label [mc "Show History Context"] \
 273                -command [cb _gitkcommit]
 274        $w.ctxm add command \
 275                -label [mc "Blame Parent Commit"] \
 276                -command [cb _blameparent]
 277
 278        foreach i $w_columns {
 279                for {set g 0} {$g < [llength $group_colors]} {incr g} {
 280                        $i tag conf color$g -background [lindex $group_colors $g]
 281                }
 282
 283                $i conf -cursor $cursor_ptr
 284                $i conf -yscrollcommand [list many2scrollbar \
 285                        $w_columns yview $w.file_pane.out.sby]
 286                bind $i <Button-1> "
 287                        [cb _hide_tooltip]
 288                        [cb _click $i @%x,%y]
 289                        focus $i
 290                "
 291                bind $i <Any-Motion>  [cb _show_tooltip $i @%x,%y]
 292                bind $i <Any-Enter>   [cb _hide_tooltip]
 293                bind $i <Any-Leave>   [cb _hide_tooltip]
 294                bind_button3 $i "
 295                        [cb _hide_tooltip]
 296                        set cursorX %x
 297                        set cursorY %y
 298                        set cursorW %W
 299                        tk_popup $w.ctxm %X %Y
 300                "
 301                bind $i <Shift-Tab> "[list focus $w_cviewer];break"
 302                bind $i <Tab>       "[list focus $w_cviewer];break"
 303        }
 304
 305        foreach i [concat $w_columns $w_cviewer] {
 306                bind $i <Key-Up>        {catch {%W yview scroll -1 units};break}
 307                bind $i <Key-Down>      {catch {%W yview scroll  1 units};break}
 308                bind $i <Key-Left>      {catch {%W xview scroll -1 units};break}
 309                bind $i <Key-Right>     {catch {%W xview scroll  1 units};break}
 310                bind $i <Key-k>         {catch {%W yview scroll -1 units};break}
 311                bind $i <Key-j>         {catch {%W yview scroll  1 units};break}
 312                bind $i <Key-h>         {catch {%W xview scroll -1 units};break}
 313                bind $i <Key-l>         {catch {%W xview scroll  1 units};break}
 314                bind $i <Control-Key-b> {catch {%W yview scroll -1 pages};break}
 315                bind $i <Control-Key-f> {catch {%W yview scroll  1 pages};break}
 316        }
 317
 318        bind $w_cviewer <Shift-Tab> "[list focus $w_file];break"
 319        bind $w_cviewer <Tab>       "[list focus $w_file];break"
 320        bind $w_cviewer <Button-1> [list focus $w_cviewer]
 321        bind $w_file    <Visibility> [list focus $w_file]
 322
 323        grid configure $w.header -sticky ew
 324        grid configure $w.file_pane -sticky nsew
 325        grid configure $w.status -sticky ew
 326        grid columnconfigure $top 0 -weight 1
 327        grid rowconfigure $top 0 -weight 0
 328        grid rowconfigure $top 1 -weight 1
 329        grid rowconfigure $top 2 -weight 0
 330
 331        set req_w [winfo reqwidth  $top]
 332        set req_h [winfo reqheight $top]
 333        set scr_w [expr {[winfo screenwidth $top] - 40}]
 334        set scr_h [expr {[winfo screenheight $top] - 120}]
 335        set opt_w [expr {$font_w * (80 + 5*3 + 3)}]
 336        if {$req_w < $opt_w} {set req_w $opt_w}
 337        if {$req_w > $scr_w} {set req_w $scr_w}
 338        set opt_h [expr {$req_w*4/3}]
 339        if {$req_h < $scr_h} {set req_h $scr_h}
 340        if {$req_h > $opt_h} {set req_h $opt_h}
 341        set g "${req_w}x${req_h}"
 342        wm geometry $top $g
 343        update
 344
 345        set old_height [winfo height $w.file_pane]
 346        $w.file_pane sash place 0 \
 347                [lindex [$w.file_pane sash coord 0] 0] \
 348                [expr {int($old_height * 0.80)}]
 349        bind $w.file_pane <Configure> \
 350        "if {{$w.file_pane} eq {%W}} {[cb _resize %h]}"
 351
 352        wm protocol $top WM_DELETE_WINDOW "destroy $top"
 353        bind $top <Destroy> [cb _kill]
 354
 355        _load $this $i_jump
 356}
 357
 358method _kill {} {
 359        if {$current_fd ne {}} {
 360                kill_file_process $current_fd
 361                catch {close $current_fd}
 362                set current_fd {}
 363        }
 364}
 365
 366method _load {jump} {
 367        variable group_colors
 368
 369        _hide_tooltip $this
 370
 371        if {$total_lines != 0 || $current_fd ne {}} {
 372                _kill $this
 373
 374                foreach i $w_columns {
 375                        $i conf -state normal
 376                        $i delete 0.0 end
 377                        foreach g [$i tag names] {
 378                                if {[regexp {^g[0-9a-f]{40}$} $g]} {
 379                                        $i tag delete $g
 380                                }
 381                        }
 382                        $i conf -state disabled
 383                }
 384
 385                $w_cviewer conf -state normal
 386                $w_cviewer delete 0.0 end
 387                $w_cviewer conf -state disabled
 388
 389                set highlight_line -1
 390                set highlight_column {}
 391                set highlight_commit {}
 392                set total_lines 0
 393        }
 394
 395        if {$history eq {}} {
 396                $w_back conf -state disabled
 397        } else {
 398                $w_back conf -state normal
 399        }
 400
 401        # Index 0 is always empty.  There is never line 0 as
 402        # we use only 1 based lines, as that matches both with
 403        # git-blame output and with Tk's text widget.
 404        #
 405        set amov_data [list [list]]
 406        set asim_data [list [list]]
 407
 408        $status show [mc "Reading %s..." "$commit:[escape_path $path]"]
 409        $w_path conf -text [escape_path $path]
 410        if {$commit eq {}} {
 411                set fd [open $path r]
 412                fconfigure $fd -eofchar {}
 413        } else {
 414                set fd [git_read cat-file blob "$commit:$path"]
 415        }
 416        fconfigure $fd \
 417                -blocking 0 \
 418                -translation lf \
 419                -encoding [get_path_encoding $path]
 420        fileevent $fd readable [cb _read_file $fd $jump]
 421        set current_fd $fd
 422}
 423
 424method _history_menu {} {
 425        set m $w.backmenu
 426        if {[winfo exists $m]} {
 427                $m delete 0 end
 428        } else {
 429                menu $m -tearoff 0
 430        }
 431
 432        for {set i [expr {[llength $history] - 1}]
 433                } {$i >= 0} {incr i -1} {
 434                set e [lindex $history $i]
 435                set c [lindex $e 0]
 436                set f [lindex $e 1]
 437
 438                if {[regexp {^[0-9a-f]{40}$} $c]} {
 439                        set t [string range $c 0 8]...
 440                } elseif {$c eq {}} {
 441                        set t {Working Directory}
 442                } else {
 443                        set t $c
 444                }
 445                if {![catch {set summary $header($c,summary)}]} {
 446                        append t " $summary"
 447                        if {[string length $t] > 70} {
 448                                set t [string range $t 0 66]...
 449                        }
 450                }
 451
 452                $m add command -label $t -command [cb _goback $i]
 453        }
 454        set X [winfo rootx $w_back]
 455        set Y [expr {[winfo rooty $w_back] + [winfo height $w_back]}]
 456        tk_popup $m $X $Y
 457}
 458
 459method _goback {i} {
 460        set dat [lindex $history $i]
 461        set history [lrange $history 0 [expr {$i - 1}]]
 462        set commit [lindex $dat 0]
 463        set path [lindex $dat 1]
 464        _load $this [lrange $dat 2 5]
 465}
 466
 467method _read_file {fd jump} {
 468        if {$fd ne $current_fd} {
 469                catch {close $fd}
 470                return
 471        }
 472
 473        foreach i $w_columns {$i conf -state normal}
 474        while {[gets $fd line] >= 0} {
 475                regsub "\r\$" $line {} line
 476                incr total_lines
 477                lappend amov_data {}
 478                lappend asim_data {}
 479
 480                if {$total_lines > 1} {
 481                        foreach i $w_columns {$i insert end "\n"}
 482                }
 483
 484                $w_line insert end "$total_lines" linenumber
 485                $w_file insert end "$line"
 486        }
 487
 488        set ln_wc [expr {[string length $total_lines] + 2}]
 489        if {[$w_line cget -width] < $ln_wc} {
 490                $w_line conf -width $ln_wc
 491        }
 492
 493        foreach i $w_columns {$i conf -state disabled}
 494
 495        if {[eof $fd]} {
 496                close $fd
 497
 498                # If we don't force Tk to update the widgets *right now*
 499                # none of our jump commands will cause a change in the UI.
 500                #
 501                update
 502
 503                if {[llength $jump] == 1} {
 504                        set highlight_line [lindex $jump 0]
 505                        $w_file see "$highlight_line.0"
 506                } elseif {[llength $jump] == 4} {
 507                        set highlight_column [lindex $jump 0]
 508                        set highlight_line [lindex $jump 1]
 509                        $w_file xview moveto [lindex $jump 2]
 510                        $w_file yview moveto [lindex $jump 3]
 511                }
 512
 513                _exec_blame $this $w_asim @asim_data \
 514                        [list] \
 515                        [mc "Loading copy/move tracking annotations..."]
 516        }
 517} ifdeleted { catch {close $fd} }
 518
 519method _exec_blame {cur_w cur_d options cur_s} {
 520        lappend options --incremental
 521        if {$commit eq {}} {
 522                lappend options --contents $path
 523        } else {
 524                lappend options $commit
 525        }
 526        lappend options -- $path
 527        set fd [eval git_read --nice blame $options]
 528        fconfigure $fd -blocking 0 -translation lf -encoding utf-8
 529        fileevent $fd readable [cb _read_blame $fd $cur_w $cur_d]
 530        set current_fd $fd
 531        set blame_lines 0
 532
 533        $status start \
 534                $cur_s \
 535                [mc "lines annotated"]
 536}
 537
 538method _read_blame {fd cur_w cur_d} {
 539        upvar #0 $cur_d line_data
 540        variable group_colors
 541
 542        if {$fd ne $current_fd} {
 543                catch {close $fd}
 544                return
 545        }
 546
 547        $cur_w conf -state normal
 548        while {[gets $fd line] >= 0} {
 549                if {[regexp {^([a-z0-9]{40}) (\d+) (\d+) (\d+)$} $line line \
 550                        cmit original_line final_line line_count]} {
 551                        set r_commit     $cmit
 552                        set r_orig_line  $original_line
 553                        set r_final_line $final_line
 554                        set r_line_count $line_count
 555                } elseif {[string match {filename *} $line]} {
 556                        set file [string range $line 9 end]
 557                        set n    $r_line_count
 558                        set lno  $r_final_line
 559                        set oln  $r_orig_line
 560                        set cmit $r_commit
 561
 562                        if {[regexp {^0{40}$} $cmit]} {
 563                                set commit_abbr work
 564                                set commit_type curr_commit
 565                        } elseif {$cmit eq $commit} {
 566                                set commit_abbr this
 567                                set commit_type curr_commit
 568                        } else {
 569                                set commit_type prior_commit
 570                                set commit_abbr [string range $cmit 0 3]
 571                        }
 572
 573                        set author_abbr {}
 574                        set a_name {}
 575                        catch {set a_name $header($cmit,author)}
 576                        while {$a_name ne {}} {
 577                                if {$author_abbr ne {}
 578                                        && [string index $a_name 0] eq {'}} {
 579                                        regsub {^'[^']+'\s+} $a_name {} a_name
 580                                }
 581                                if {![regexp {^([[:upper:]])} $a_name _a]} break
 582                                append author_abbr $_a
 583                                unset _a
 584                                if {![regsub \
 585                                        {^[[:upper:]][^\s]*\s+} \
 586                                        $a_name {} a_name ]} break
 587                        }
 588                        if {$author_abbr eq {}} {
 589                                set author_abbr { |}
 590                        } else {
 591                                set author_abbr [string range $author_abbr 0 3]
 592                        }
 593                        unset a_name
 594
 595                        set first_lno $lno
 596                        while {
 597                           $first_lno > 1
 598                        && $cmit eq [lindex $line_data [expr {$first_lno - 1}] 0]
 599                        && $file eq [lindex $line_data [expr {$first_lno - 1}] 1]
 600                        } {
 601                                incr first_lno -1
 602                        }
 603
 604                        set color {}
 605                        if {$first_lno < $lno} {
 606                                foreach g [$w_file tag names $first_lno.0] {
 607                                        if {[regexp {^color[0-9]+$} $g]} {
 608                                                set color $g
 609                                                break
 610                                        }
 611                                }
 612                        } else {
 613                                set i [lsort [concat \
 614                                        [$w_file tag names "[expr {$first_lno - 1}].0"] \
 615                                        [$w_file tag names "[expr {$lno + $n}].0"] \
 616                                        ]]
 617                                for {set g 0} {$g < [llength $group_colors]} {incr g} {
 618                                        if {[lsearch -sorted -exact $i color$g] == -1} {
 619                                                set color color$g
 620                                                break
 621                                        }
 622                                }
 623                        }
 624                        if {$color eq {}} {
 625                                set color color0
 626                        }
 627
 628                        while {$n > 0} {
 629                                set lno_e "$lno.0 lineend + 1c"
 630                                if {[lindex $line_data $lno] ne {}} {
 631                                        set g [lindex $line_data $lno 0]
 632                                        foreach i $w_columns {
 633                                                $i tag remove g$g $lno.0 $lno_e
 634                                        }
 635                                }
 636                                lset line_data $lno [list $cmit $file $oln]
 637
 638                                $cur_w delete $lno.0 "$lno.0 lineend"
 639                                if {$lno == $first_lno} {
 640                                        $cur_w insert $lno.0 $commit_abbr $commit_type
 641                                } elseif {$lno == [expr {$first_lno + 1}]} {
 642                                        $cur_w insert $lno.0 $author_abbr author_abbr
 643                                } else {
 644                                        $cur_w insert $lno.0 { |}
 645                                }
 646
 647                                foreach i $w_columns {
 648                                        if {$cur_w eq $w_amov} {
 649                                                for {set g 0} \
 650                                                        {$g < [llength $group_colors]} \
 651                                                        {incr g} {
 652                                                        $i tag remove color$g $lno.0 $lno_e
 653                                                }
 654                                                $i tag add $color $lno.0 $lno_e
 655                                        }
 656                                        $i tag add g$cmit $lno.0 $lno_e
 657                                }
 658
 659                                if {$highlight_column eq $cur_w} {
 660                                        if {$highlight_line == -1
 661                                         && [lindex [$w_file yview] 0] == 0} {
 662                                                $w_file see $lno.0
 663                                                set highlight_line $lno
 664                                        }
 665                                        if {$highlight_line == $lno} {
 666                                                _showcommit $this $cur_w $lno
 667                                        }
 668                                }
 669
 670                                incr n -1
 671                                incr lno
 672                                incr oln
 673                                incr blame_lines
 674                        }
 675
 676                        while {
 677                           $cmit eq [lindex $line_data $lno 0]
 678                        && $file eq [lindex $line_data $lno 1]
 679                        } {
 680                                $cur_w delete $lno.0 "$lno.0 lineend"
 681
 682                                if {$lno == $first_lno} {
 683                                        $cur_w insert $lno.0 $commit_abbr $commit_type
 684                                } elseif {$lno == [expr {$first_lno + 1}]} {
 685                                        $cur_w insert $lno.0 $author_abbr author_abbr
 686                                } else {
 687                                        $cur_w insert $lno.0 { |}
 688                                }
 689
 690                                if {$cur_w eq $w_amov} {
 691                                        foreach i $w_columns {
 692                                                for {set g 0} \
 693                                                        {$g < [llength $group_colors]} \
 694                                                        {incr g} {
 695                                                        $i tag remove color$g $lno.0 $lno_e
 696                                                }
 697                                                $i tag add $color $lno.0 $lno_e
 698                                        }
 699                                }
 700
 701                                incr lno
 702                        }
 703
 704                } elseif {[regexp {^([a-z-]+) (.*)$} $line line key data]} {
 705                        set header($r_commit,$key) $data
 706                }
 707        }
 708        $cur_w conf -state disabled
 709
 710        if {[eof $fd]} {
 711                close $fd
 712                if {$cur_w eq $w_asim} {
 713                        # Switches for original location detection
 714                        set threshold [get_config gui.copyblamethreshold]
 715                        set original_options [list "-C$threshold"]
 716
 717                        if {![is_config_true gui.fastcopyblame]} {
 718                                # thorough copy search; insert before the threshold
 719                                set original_options [linsert $original_options 0 -C]
 720                        }
 721                        if {[git-version >= 1.5.3]} {
 722                                lappend original_options -w ; # ignore indentation changes
 723                        }
 724
 725                        _exec_blame $this $w_amov @amov_data \
 726                                $original_options \
 727                                [mc "Loading original location annotations..."]
 728                } else {
 729                        set current_fd {}
 730                        $status stop [mc "Annotation complete."]
 731                }
 732        } else {
 733                $status update $blame_lines $total_lines
 734        }
 735} ifdeleted { catch {close $fd} }
 736
 737method _find_commit_bound {data_list start_idx delta} {
 738        upvar #0 $data_list line_data
 739        set pos $start_idx
 740        set limit       [expr {[llength $line_data] - 1}]
 741        set base_commit [lindex $line_data $pos 0]
 742
 743        while {$pos > 0 && $pos < $limit} {
 744                set new_pos [expr {$pos + $delta}]
 745                if {[lindex $line_data $new_pos 0] ne $base_commit} {
 746                        return $pos
 747                }
 748
 749                set pos $new_pos
 750        }
 751
 752        return $pos
 753}
 754
 755method _fullcopyblame {} {
 756        if {$current_fd ne {}} {
 757                tk_messageBox \
 758                        -icon error \
 759                        -type ok \
 760                        -title [mc "Busy"] \
 761                        -message [mc "Annotation process is already running."]
 762
 763                return
 764        }
 765
 766        # Switches for original location detection
 767        set threshold [get_config gui.copyblamethreshold]
 768        set original_options [list -C -C "-C$threshold"]
 769
 770        if {[git-version >= 1.5.3]} {
 771                lappend original_options -w ; # ignore indentation changes
 772        }
 773
 774        # Find the line range
 775        set pos @$::cursorX,$::cursorY
 776        set lno [lindex [split [$::cursorW index $pos] .] 0]
 777        set min_amov_lno [_find_commit_bound $this @amov_data $lno -1]
 778        set max_amov_lno [_find_commit_bound $this @amov_data $lno 1]
 779        set min_asim_lno [_find_commit_bound $this @asim_data $lno -1]
 780        set max_asim_lno [_find_commit_bound $this @asim_data $lno 1]
 781
 782        if {$min_asim_lno < $min_amov_lno} {
 783                set min_amov_lno $min_asim_lno
 784        }
 785
 786        if {$max_asim_lno > $max_amov_lno} {
 787                set max_amov_lno $max_asim_lno
 788        }
 789
 790        lappend original_options -L "$min_amov_lno,$max_amov_lno"
 791
 792        # Clear lines
 793        for {set i $min_amov_lno} {$i <= $max_amov_lno} {incr i} {
 794                lset amov_data $i [list ]
 795        }
 796
 797        # Start the back-end process
 798        _exec_blame $this $w_amov @amov_data \
 799                $original_options \
 800                [mc "Running thorough copy detection..."]
 801}
 802
 803method _click {cur_w pos} {
 804        set lno [lindex [split [$cur_w index $pos] .] 0]
 805        _showcommit $this $cur_w $lno
 806}
 807
 808method _setencoding {enc} {
 809        force_path_encoding $path $enc
 810        _load $this [list \
 811                $highlight_column \
 812                $highlight_line \
 813                [lindex [$w_file xview] 0] \
 814                [lindex [$w_file yview] 0] \
 815                ]
 816}
 817
 818method _load_commit {cur_w cur_d pos} {
 819        upvar #0 $cur_d line_data
 820        set lno [lindex [split [$cur_w index $pos] .] 0]
 821        set dat [lindex $line_data $lno]
 822        if {$dat ne {}} {
 823                _load_new_commit $this  \
 824                        [lindex $dat 0] \
 825                        [lindex $dat 1] \
 826                        [list [lindex $dat 2]]
 827        }
 828}
 829
 830method _load_new_commit {new_commit new_path jump} {
 831        lappend history [list \
 832                $commit $path \
 833                $highlight_column \
 834                $highlight_line \
 835                [lindex [$w_file xview] 0] \
 836                [lindex [$w_file yview] 0] \
 837                ]
 838
 839        set commit $new_commit
 840        set path   $new_path
 841        _load $this $jump
 842}
 843
 844method _showcommit {cur_w lno} {
 845        global repo_config
 846        variable active_color
 847
 848        if {$highlight_commit ne {}} {
 849                foreach i $w_columns {
 850                        $i tag conf g$highlight_commit -background {}
 851                        $i tag lower g$highlight_commit
 852                }
 853        }
 854
 855        if {$cur_w eq $w_asim} {
 856                set dat [lindex $asim_data $lno]
 857                set highlight_column $w_asim
 858        } else {
 859                set dat [lindex $amov_data $lno]
 860                set highlight_column $w_amov
 861        }
 862
 863        $w_cviewer conf -state normal
 864        $w_cviewer delete 0.0 end
 865
 866        if {$dat eq {}} {
 867                set cmit {}
 868                $w_cviewer insert end [mc "Loading annotation..."] still_loading
 869        } else {
 870                set cmit [lindex $dat 0]
 871                set file [lindex $dat 1]
 872
 873                foreach i $w_columns {
 874                        $i tag conf g$cmit -background $active_color
 875                        $i tag raise g$cmit
 876                }
 877
 878                set author_name {}
 879                set author_email {}
 880                set author_time {}
 881                catch {set author_name $header($cmit,author)}
 882                catch {set author_email $header($cmit,author-mail)}
 883                catch {set author_time [format_date $header($cmit,author-time)]}
 884
 885                set committer_name {}
 886                set committer_email {}
 887                set committer_time {}
 888                catch {set committer_name $header($cmit,committer)}
 889                catch {set committer_email $header($cmit,committer-mail)}
 890                catch {set committer_time [format_date $header($cmit,committer-time)]}
 891
 892                if {[catch {set msg $header($cmit,message)}]} {
 893                        set msg {}
 894                        catch {
 895                                set fd [git_read cat-file commit $cmit]
 896                                fconfigure $fd -encoding binary -translation lf
 897                                if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
 898                                        set enc utf-8
 899                                }
 900                                while {[gets $fd line] > 0} {
 901                                        if {[string match {encoding *} $line]} {
 902                                                set enc [string tolower [string range $line 9 end]]
 903                                        }
 904                                }
 905                                set msg [read $fd]
 906                                close $fd
 907
 908                                set enc [tcl_encoding $enc]
 909                                if {$enc ne {}} {
 910                                        set msg [encoding convertfrom $enc $msg]
 911                                }
 912                                set msg [string trim $msg]
 913                        }
 914                        set header($cmit,message) $msg
 915                }
 916
 917                $w_cviewer insert end "commit $cmit\n" header_key
 918                $w_cviewer insert end [strcat [mc "Author:"] "\t"] header_key
 919                $w_cviewer insert end "$author_name $author_email" header_val
 920                $w_cviewer insert end "  $author_time\n" header_val
 921
 922                $w_cviewer insert end [strcat [mc "Committer:"] "\t"] header_key
 923                $w_cviewer insert end "$committer_name $committer_email" header_val
 924                $w_cviewer insert end "  $committer_time\n" header_val
 925
 926                if {$file ne $path} {
 927                        $w_cviewer insert end [strcat [mc "Original File:"] "\t"] header_key
 928                        $w_cviewer insert end "[escape_path $file]\n" header_val
 929                }
 930
 931                $w_cviewer insert end "\n$msg"
 932        }
 933        $w_cviewer conf -state disabled
 934
 935        set highlight_line $lno
 936        set highlight_commit $cmit
 937
 938        if {[lsearch -exact $tooltip_commit $highlight_commit] != -1} {
 939                _hide_tooltip $this
 940        }
 941}
 942
 943method _get_click_amov_info {} {
 944        set pos @$::cursorX,$::cursorY
 945        set lno [lindex [split [$::cursorW index $pos] .] 0]
 946        return [lindex $amov_data $lno]
 947}
 948
 949method _copycommit {} {
 950        set dat [_get_click_amov_info $this]
 951        if {$dat ne {}} {
 952                clipboard clear
 953                clipboard append \
 954                        -format STRING \
 955                        -type STRING \
 956                        -- [lindex $dat 0]
 957        }
 958}
 959
 960method _format_offset_date {base offset} {
 961        set exval [expr {$base + $offset*24*60*60}]
 962        return [clock format $exval -format {%Y-%m-%d}]
 963}
 964
 965method _gitkcommit {} {
 966        global nullid
 967
 968        set dat [_get_click_amov_info $this]
 969        if {$dat ne {}} {
 970                set cmit [lindex $dat 0]
 971
 972                # If the line belongs to the working copy, use HEAD instead
 973                if {$cmit eq $nullid} {
 974                        if {[catch {set cmit [git rev-parse --verify HEAD]} err]} {
 975                                error_popup [strcat [mc "Cannot find HEAD commit:"] "\n\n$err"]
 976                                return;
 977                        }
 978                }
 979
 980                set radius [get_config gui.blamehistoryctx]
 981                set cmdline [list --select-commit=$cmit]
 982
 983                if {$radius > 0} {
 984                        set author_time {}
 985                        set committer_time {}
 986
 987                        catch {set author_time $header($cmit,author-time)}
 988                        catch {set committer_time $header($cmit,committer-time)}
 989
 990                        if {$committer_time eq {}} {
 991                                set committer_time $author_time
 992                        }
 993
 994                        set after_time [_format_offset_date $this $committer_time [expr {-$radius}]]
 995                        set before_time [_format_offset_date $this $committer_time $radius]
 996
 997                        lappend cmdline --after=$after_time --before=$before_time
 998                }
 999
1000                lappend cmdline $cmit
1001
1002                set base_rev "HEAD"
1003                if {$commit ne {}} {
1004                        set base_rev $commit
1005                }
1006
1007                if {$base_rev ne $cmit} {
1008                        lappend cmdline $base_rev
1009                }
1010
1011                do_gitk $cmdline
1012        }
1013}
1014
1015method _blameparent {} {
1016        global nullid
1017
1018        set dat [_get_click_amov_info $this]
1019        if {$dat ne {}} {
1020                set cmit [lindex $dat 0]
1021                set new_path [lindex $dat 1]
1022
1023                # Allow using Blame Parent on lines modified in the working copy
1024                if {$cmit eq $nullid} {
1025                        set parent_ref "HEAD"
1026                } else {
1027                        set parent_ref "$cmit^"
1028                }
1029                if {[catch {set cparent [git rev-parse --verify $parent_ref]} err]} {
1030                        error_popup [strcat [mc "Cannot find parent commit:"] "\n\n$err"]
1031                        return;
1032                }
1033
1034                _kill $this
1035
1036                # Generate a diff between the commit and its parent,
1037                # and use the hunks to update the line number.
1038                # Request zero context to simplify calculations.
1039                if {$cmit eq $nullid} {
1040                        set diffcmd [list diff-index --unified=0 $cparent -- $new_path]
1041                } else {
1042                        set diffcmd [list diff-tree --unified=0 $cparent $cmit -- $new_path]
1043                }
1044                if {[catch {set fd [eval git_read $diffcmd]} err]} {
1045                        $status stop [mc "Unable to display parent"]
1046                        error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
1047                        return
1048                }
1049
1050                set r_orig_line [lindex $dat 2]
1051
1052                fconfigure $fd \
1053                        -blocking 0 \
1054                        -encoding binary \
1055                        -translation binary
1056                fileevent $fd readable [cb _read_diff_load_commit \
1057                        $fd $cparent $new_path $r_orig_line]
1058                set current_fd $fd
1059        }
1060}
1061
1062method _read_diff_load_commit {fd cparent new_path tline} {
1063        if {$fd ne $current_fd} {
1064                catch {close $fd}
1065                return
1066        }
1067
1068        while {[gets $fd line] >= 0} {
1069                if {[regexp {^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@} $line line \
1070                        old_line osz old_size new_line nsz new_size]} {
1071
1072                        if {$osz eq {}} { set old_size 1 }
1073                        if {$nsz eq {}} { set new_size 1 }
1074
1075                        if {$new_line <= $tline} {
1076                                if {[expr {$new_line + $new_size}] > $tline} {
1077                                        # Target line within the hunk
1078                                        set line_shift [expr {
1079                                                ($new_size-$old_size)*($tline-$new_line)/$new_size
1080                                                }]
1081                                } else {
1082                                        set line_shift [expr {$new_size-$old_size}]
1083                                }
1084
1085                                set r_orig_line [expr {$r_orig_line - $line_shift}]
1086                        }
1087                }
1088        }
1089
1090        if {[eof $fd]} {
1091                close $fd;
1092                set current_fd {}
1093
1094                _load_new_commit $this  \
1095                        $cparent        \
1096                        $new_path       \
1097                        [list $r_orig_line]
1098        }
1099} ifdeleted { catch {close $fd} }
1100
1101method _show_tooltip {cur_w pos} {
1102        if {$tooltip_wm ne {}} {
1103                _open_tooltip $this $cur_w
1104        } elseif {$tooltip_timer eq {}} {
1105                set tooltip_timer [after 1000 [cb _open_tooltip $cur_w]]
1106        }
1107}
1108
1109method _open_tooltip {cur_w} {
1110        set tooltip_timer {}
1111        set pos_x [winfo pointerx $cur_w]
1112        set pos_y [winfo pointery $cur_w]
1113        if {[winfo containing $pos_x $pos_y] ne $cur_w} {
1114                _hide_tooltip $this
1115                return
1116        }
1117
1118        if {$tooltip_wm ne "$cur_w.tooltip"} {
1119                _hide_tooltip $this
1120
1121                set tooltip_wm [toplevel $cur_w.tooltip -borderwidth 1]
1122                wm overrideredirect $tooltip_wm 1
1123                wm transient $tooltip_wm [winfo toplevel $cur_w]
1124                set tooltip_t $tooltip_wm.label
1125                text $tooltip_t \
1126                        -takefocus 0 \
1127                        -highlightthickness 0 \
1128                        -relief flat \
1129                        -borderwidth 0 \
1130                        -wrap none \
1131                        -background lightyellow \
1132                        -foreground black
1133                $tooltip_t tag conf section_header -font font_uibold
1134                pack $tooltip_t
1135        } else {
1136                $tooltip_t conf -state normal
1137                $tooltip_t delete 0.0 end
1138        }
1139
1140        set pos @[join [list \
1141                [expr {$pos_x - [winfo rootx $cur_w]}] \
1142                [expr {$pos_y - [winfo rooty $cur_w]}]] ,]
1143        set lno [lindex [split [$cur_w index $pos] .] 0]
1144        if {$cur_w eq $w_amov} {
1145                set dat [lindex $amov_data $lno]
1146                set org {}
1147        } else {
1148                set dat [lindex $asim_data $lno]
1149                set org [lindex $amov_data $lno]
1150        }
1151
1152        if {$dat eq {}} {
1153                _hide_tooltip $this
1154                return
1155        }
1156
1157        set cmit [lindex $dat 0]
1158        set tooltip_commit [list $cmit]
1159
1160        set author_name {}
1161        set summary     {}
1162        set author_time {}
1163        catch {set author_name $header($cmit,author)}
1164        catch {set summary     $header($cmit,summary)}
1165        catch {set author_time [format_date $header($cmit,author-time)]}
1166
1167        $tooltip_t insert end "commit $cmit\n"
1168        $tooltip_t insert end "$author_name  $author_time\n"
1169        $tooltip_t insert end "$summary"
1170
1171        if {$org ne {} && [lindex $org 0] ne $cmit} {
1172                set save [$tooltip_t get 0.0 end]
1173                $tooltip_t delete 0.0 end
1174
1175                set cmit [lindex $org 0]
1176                set file [lindex $org 1]
1177                lappend tooltip_commit $cmit
1178
1179                set author_name {}
1180                set summary     {}
1181                set author_time {}
1182                catch {set author_name $header($cmit,author)}
1183                catch {set summary     $header($cmit,summary)}
1184                catch {set author_time [format_date $header($cmit,author-time)]}
1185
1186                $tooltip_t insert end [strcat [mc "Originally By:"] "\n"] section_header
1187                $tooltip_t insert end "commit $cmit\n"
1188                $tooltip_t insert end "$author_name  $author_time\n"
1189                $tooltip_t insert end "$summary\n"
1190
1191                if {$file ne $path} {
1192                        $tooltip_t insert end [strcat [mc "In File:"] " "] section_header
1193                        $tooltip_t insert end "$file\n"
1194                }
1195
1196                $tooltip_t insert end "\n"
1197                $tooltip_t insert end [strcat [mc "Copied Or Moved Here By:"] "\n"] section_header
1198                $tooltip_t insert end $save
1199        }
1200
1201        $tooltip_t conf -state disabled
1202        _position_tooltip $this
1203}
1204
1205method _position_tooltip {} {
1206        set max_h [lindex [split [$tooltip_t index end] .] 0]
1207        set max_w 0
1208        for {set i 1} {$i <= $max_h} {incr i} {
1209                set c [lindex [split [$tooltip_t index "$i.0 lineend"] .] 1]
1210                if {$c > $max_w} {set max_w $c}
1211        }
1212        $tooltip_t conf -width $max_w -height $max_h
1213
1214        set req_w [winfo reqwidth  $tooltip_t]
1215        set req_h [winfo reqheight $tooltip_t]
1216        set pos_x [expr {[winfo pointerx .] +  5}]
1217        set pos_y [expr {[winfo pointery .] + 10}]
1218
1219        set g "${req_w}x${req_h}"
1220        if {$pos_x >= 0} {append g +}
1221        append g $pos_x
1222        if {$pos_y >= 0} {append g +}
1223        append g $pos_y
1224
1225        wm geometry $tooltip_wm $g
1226        raise $tooltip_wm
1227}
1228
1229method _hide_tooltip {} {
1230        if {$tooltip_wm ne {}} {
1231                destroy $tooltip_wm
1232                set tooltip_wm {}
1233                set tooltip_commit {}
1234        }
1235        if {$tooltip_timer ne {}} {
1236                after cancel $tooltip_timer
1237                set tooltip_timer {}
1238        }
1239}
1240
1241method _resize {new_height} {
1242        set diff [expr {$new_height - $old_height}]
1243        if {$diff == 0} return
1244
1245        set my [expr {[winfo height $w.file_pane] - 25}]
1246        set o [$w.file_pane sash coord 0]
1247        set ox [lindex $o 0]
1248        set oy [expr {[lindex $o 1] + $diff}]
1249        if {$oy < 0}   {set oy 0}
1250        if {$oy > $my} {set oy $my}
1251        $w.file_pane sash place 0 $ox $oy
1252
1253        set old_height $new_height
1254}
1255
1256}