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