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