1
+ <?php
2
+
3
+ class PHP_CodeCoverage_Git_Blame
4
+ {
5
+ public $ rev ;
6
+ public $ author ;
7
+ public $ lineNo ;
8
+ public $ linesLeftInBlameGroup ;
9
+ public $ isStartGroupLine ;
10
+
11
+ /**
12
+ * Returns an array of line number to blame info
13
+ *
14
+ * @param $file
15
+ * @param $lines
16
+ * @throws PHP_CodeCoverage_Exception
17
+ * @return array of PHP_CodeCoverage_Git_Blame
18
+ */
19
+ public static function getBlameInfo ($ file )
20
+ {
21
+ $ blameOutput = self ::getBlameOutput ($ file );
22
+ $ ret = array ();
23
+ $ i = 0 ;
24
+ $ linesLeftInBlameGroup = 1 ;
25
+ $ blameLine = current ($ blameOutput );
26
+ while (false !== $ blameLine ) {
27
+ $ i ++;
28
+ $ linesLeftInBlameGroup --;
29
+ $ isStartGroupLine = !$ linesLeftInBlameGroup ;
30
+ if ($ isStartGroupLine ) {
31
+ if (!preg_match ('/^(.{40}) (\d+) (\d+) (\d+)$/ ' , $ blameLine , $ matches )) {
32
+ throw new PHP_CodeCoverage_Exception (
33
+ "Unexpected output from git blame for file line $ i: " . $ blameLine );
34
+ }
35
+ $ linesLeftInBlameGroup = $ matches [4 ];
36
+ } else {
37
+ if (!preg_match ('/^(.{40}) (\d+) (\d+)$/ ' , $ blameLine , $ matches )) {
38
+ throw new PHP_CodeCoverage_Exception (
39
+ "Unexpected output from git blame for file line $ i: " . $ blameLine );
40
+ }
41
+ }
42
+ $ sha = $ matches [1 ];
43
+ if ($ i != $ matches [3 ]) {
44
+ throw new PHP_CodeCoverage_Exception (
45
+ "Unexpected output from git blame (expected line $ i): " . $ blameLine );
46
+ }
47
+
48
+ $ author = '' ;
49
+ do
50
+ {
51
+ $ blameData = next ($ blameOutput );
52
+ if (0 === strpos ($ blameData , 'author ' )) {
53
+ $ author = substr ($ blameData , strlen ('author ' ));
54
+ }
55
+ } while (
56
+ false !== $ blameData && // EOF
57
+ 0 !== strpos ($ blameData , "\t" ) && // End of line info
58
+ '' !== $ blameData ); // End of line info which has been trim()ed by exec
59
+
60
+ $ info = new PHP_CodeCoverage_Git_Blame ();
61
+ $ info ->rev = $ sha ;
62
+ $ info ->author = $ author ;
63
+ $ info ->lineNo = $ i ;
64
+ $ info ->linesLeftInBlameGroup = $ linesLeftInBlameGroup ;
65
+ $ info ->isStartGroupLine = $ isStartGroupLine ;
66
+ $ ret [$ i ] = $ info ;
67
+
68
+ $ blameLine = next ($ blameOutput );
69
+ }
70
+ return $ ret ;
71
+ }
72
+
73
+ /**
74
+ * Runs "git blame" on the given file and returns the output.
75
+ *
76
+ * @param $file
77
+ * @return array of strings - the "porcelain" output from "git blame"
78
+ * @throws PHP_CodeCoverage_Exception
79
+ */
80
+ protected static function getBlameOutput ($ file )
81
+ {
82
+ if (strpos (`uname `, 'CYGWIN ' ) !== false ) {
83
+ // Cygwin git does not like absolute Windows filenames
84
+ $ file = `cygpath " $ file" `;
85
+ }
86
+ $ file = escapeshellarg ($ file );
87
+ $ cmd = "git blame -p $ file " ;
88
+ exec ($ cmd , $ blameOutput , $ retval );
89
+ if ($ retval != 0 ) {
90
+ throw new PHP_CodeCoverage_Exception (
91
+ "Error running git blame. Perhaps you do not have git installed, or perhaps the " .
92
+ "project is not in git source control. Please either fix this error or don't use " .
93
+ "the 'excludeCommits' option. \nCommand: $ cmd \nOutput: $ blameOutput " );
94
+ }
95
+ return $ blameOutput ;
96
+ }
97
+
98
+ }
0 commit comments