5252from git .compat import (
5353 text_type ,
5454 defenc ,
55- PY3
55+ PY3 ,
56+ safe_decode ,
5657)
5758
5859import os
5960import sys
6061import re
62+ from six .moves import range
6163
6264DefaultDBType = GitCmdObjectDB
6365if sys .version_info [:2 ] < (2 , 5 ): # python 2.4 compatiblity
@@ -655,7 +657,64 @@ def active_branch(self):
655657 :return: Head to the active branch"""
656658 return self .head .reference
657659
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 ):
659718 """The blame information for the given file at the given revision.
660719
661720 :parm rev: revision specifier, see git-rev-parse for viable options.
@@ -664,6 +723,9 @@ def blame(self, rev, file):
664723 A list of tuples associating a Commit object with a list of lines that
665724 changed within the given commit. The Commit objects will be given in order
666725 of appearance."""
726+ if incremental :
727+ return self .blame_incremental (rev , file )
728+
667729 data = self .git .blame (rev , '--' , file , p = True , stdout_as_string = False )
668730 commits = dict ()
669731 blames = list ()
0 commit comments