52
52
from git .compat import (
53
53
text_type ,
54
54
defenc ,
55
- PY3
55
+ PY3 ,
56
+ safe_decode ,
56
57
)
57
58
58
59
import os
59
60
import sys
60
61
import re
62
+ from six .moves import range
61
63
62
64
DefaultDBType = GitCmdObjectDB
63
65
if sys .version_info [:2 ] < (2 , 5 ): # python 2.4 compatiblity
@@ -655,7 +657,64 @@ def active_branch(self):
655
657
:return: Head to the active branch"""
656
658
return self .head .reference
657
659
658
- def blame (self , rev , file ):
660
+ def blame_incremental (self , rev , file , ** kwargs ):
661
+ """Iterator for blame information for the given file at the given revision.
662
+
663
+ Unlike .blame(), this does not return the actual file's contents, only
664
+ a stream of (commit, range) tuples.
665
+
666
+ :parm rev: revision specifier, see git-rev-parse for viable options.
667
+ :return: lazy iterator of (git.Commit, range) tuples, where the commit
668
+ indicates the commit to blame for the line, and range
669
+ indicates a span of line numbers in the resulting file.
670
+
671
+ If you combine all line number ranges outputted by this command, you
672
+ should get a continuous range spanning all line numbers in the file.
673
+ """
674
+ data = self .git .blame (rev , '--' , file , p = True , incremental = True , stdout_as_string = False , ** kwargs )
675
+ commits = dict ()
676
+
677
+ stream = iter (data .splitlines ())
678
+ while True :
679
+ line = next (stream ) # when exhausted, casues a StopIteration, terminating this function
680
+
681
+ hexsha , _ , lineno , num_lines = line .split ()
682
+ lineno = int (lineno )
683
+ num_lines = int (num_lines )
684
+ if hexsha not in commits :
685
+ # Now read the next few lines and build up a dict of properties
686
+ # for this commit
687
+ props = dict ()
688
+ while True :
689
+ line = next (stream )
690
+ if line == b'boundary' :
691
+ # "boundary" indicates a root commit and occurs
692
+ # instead of the "previous" tag
693
+ continue
694
+
695
+ tag , value = line .split (b' ' , 1 )
696
+ props [tag ] = value
697
+ if tag == b'filename' :
698
+ # "filename" formally terminates the entry for --incremental
699
+ break
700
+
701
+ c = Commit (self , hex_to_bin (hexsha ),
702
+ author = Actor (safe_decode (props [b'author' ]),
703
+ safe_decode (props [b'author-mail' ].lstrip (b'<' ).rstrip (b'>' ))),
704
+ authored_date = int (props [b'author-time' ]),
705
+ committer = Actor (safe_decode (props [b'committer' ]),
706
+ safe_decode (props [b'committer-mail' ].lstrip (b'<' ).rstrip (b'>' ))),
707
+ committed_date = int (props [b'committer-time' ]),
708
+ message = safe_decode (props [b'summary' ]))
709
+ commits [hexsha ] = c
710
+ else :
711
+ # Discard the next line (it's a filename end tag)
712
+ line = next (stream )
713
+ assert line .startswith (b'filename' ), 'Unexpected git blame output'
714
+
715
+ yield commits [hexsha ], range (lineno , lineno + num_lines )
716
+
717
+ def blame (self , rev , file , incremental = False , ** kwargs ):
659
718
"""The blame information for the given file at the given revision.
660
719
661
720
:parm rev: revision specifier, see git-rev-parse for viable options.
@@ -664,7 +723,10 @@ def blame(self, rev, file):
664
723
A list of tuples associating a Commit object with a list of lines that
665
724
changed within the given commit. The Commit objects will be given in order
666
725
of appearance."""
667
- data = self .git .blame (rev , '--' , file , p = True , stdout_as_string = False )
726
+ if incremental :
727
+ return self .blame_incremental (rev , file , ** kwargs )
728
+
729
+ data = self .git .blame (rev , '--' , file , p = True , stdout_as_string = False , ** kwargs )
668
730
commits = dict ()
669
731
blames = list ()
670
732
info = None
0 commit comments