Code

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