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