Diff.php 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. <?php
  2. /**
  3. * General API for generating and formatting diffs - the differences between
  4. * two sequences of strings.
  5. *
  6. * The original PHP version of this code was written by Geoffrey T. Dairiki
  7. * <dairiki@dairiki.org>, and is used/adapted with his permission.
  8. *
  9. * Copyright 2004 Geoffrey T. Dairiki <dairiki@dairiki.org>
  10. * Copyright 2004-2012 Horde LLC (http://www.horde.org/)
  11. *
  12. * See the enclosed file COPYING for license information (LGPL). If you did
  13. * not receive this file, see http://www.horde.org/licenses/lgpl21.
  14. *
  15. * @package Text_Diff
  16. * @author Geoffrey T. Dairiki <dairiki@dairiki.org>
  17. */
  18. class Horde_Text_Diff
  19. {
  20. /**
  21. * Array of changes.
  22. *
  23. * @var array
  24. */
  25. protected $_edits;
  26. /**
  27. * Computes diffs between sequences of strings.
  28. *
  29. * @param string $engine Name of the diffing engine to use. 'auto'
  30. * will automatically select the best.
  31. * @param array $params Parameters to pass to the diffing engine.
  32. * Normally an array of two arrays, each
  33. * containing the lines from a file.
  34. */
  35. public function __construct($engine, $params)
  36. {
  37. if ($engine == 'auto') {
  38. $engine = extension_loaded('xdiff') ? 'Xdiff' : 'Native';
  39. } else {
  40. $engine = Horde_String::ucfirst(basename($engine));
  41. }
  42. $class = 'Horde_Text_Diff_Engine_' . $engine;
  43. $diff_engine = new $class();
  44. $this->_edits = call_user_func_array([$diff_engine, 'diff'], $params);
  45. }
  46. /**
  47. * Returns the array of differences.
  48. */
  49. public function getDiff()
  50. {
  51. return $this->_edits;
  52. }
  53. /**
  54. * returns the number of new (added) lines in a given diff.
  55. *
  56. * @return integer The number of new lines
  57. */
  58. public function countAddedLines()
  59. {
  60. $count = 0;
  61. foreach ($this->_edits as $edit) {
  62. if ($edit instanceof Horde_Text_Diff_Op_Add ||
  63. $edit instanceof Horde_Text_Diff_Op_Change) {
  64. $count += $edit->nfinal();
  65. }
  66. }
  67. return $count;
  68. }
  69. /**
  70. * Returns the number of deleted (removed) lines in a given diff.
  71. *
  72. * @return integer The number of deleted lines
  73. */
  74. public function countDeletedLines()
  75. {
  76. $count = 0;
  77. foreach ($this->_edits as $edit) {
  78. if ($edit instanceof Horde_Text_Diff_Op_Delete ||
  79. $edit instanceof Horde_Text_Diff_Op_Change) {
  80. $count += $edit->norig();
  81. }
  82. }
  83. return $count;
  84. }
  85. /**
  86. * Computes a reversed diff.
  87. *
  88. * Example:
  89. * <code>
  90. * $diff = new Horde_Text_Diff($lines1, $lines2);
  91. * $rev = $diff->reverse();
  92. * </code>
  93. *
  94. * @return Horde_Text_Diff A Diff object representing the inverse of the
  95. * original diff. Note that we purposely don't return a
  96. * reference here, since this essentially is a clone()
  97. * method.
  98. */
  99. public function reverse()
  100. {
  101. if (version_compare(zend_version(), '2', '>')) {
  102. $rev = clone($this);
  103. } else {
  104. $rev = $this;
  105. }
  106. $rev->_edits = [];
  107. foreach ($this->_edits as $edit) {
  108. $rev->_edits[] = $edit->reverse();
  109. }
  110. return $rev;
  111. }
  112. /**
  113. * Checks for an empty diff.
  114. *
  115. * @return boolean True if two sequences were identical.
  116. */
  117. public function isEmpty()
  118. {
  119. foreach ($this->_edits as $edit) {
  120. if (!($edit instanceof Horde_Text_Diff_Op_Copy)) {
  121. return false;
  122. }
  123. }
  124. return true;
  125. }
  126. /**
  127. * Computes the length of the Longest Common Subsequence (LCS).
  128. *
  129. * This is mostly for diagnostic purposes.
  130. *
  131. * @return integer The length of the LCS.
  132. */
  133. public function lcs()
  134. {
  135. $lcs = 0;
  136. foreach ($this->_edits as $edit) {
  137. if ($edit instanceof Horde_Text_Diff_Op_Copy) {
  138. $lcs += count($edit->orig);
  139. }
  140. }
  141. return $lcs;
  142. }
  143. /**
  144. * Gets the original set of lines.
  145. *
  146. * This reconstructs the $from_lines parameter passed to the constructor.
  147. *
  148. * @return array The original sequence of strings.
  149. */
  150. public function getOriginal()
  151. {
  152. $lines = [];
  153. foreach ($this->_edits as $edit) {
  154. if ($edit->orig) {
  155. array_splice($lines, count($lines), 0, $edit->orig);
  156. }
  157. }
  158. return $lines;
  159. }
  160. /**
  161. * Gets the final set of lines.
  162. *
  163. * This reconstructs the $to_lines parameter passed to the constructor.
  164. *
  165. * @return array The sequence of strings.
  166. */
  167. public function getFinal()
  168. {
  169. $lines = [];
  170. foreach ($this->_edits as $edit) {
  171. if ($edit->final) {
  172. array_splice($lines, count($lines), 0, $edit->final);
  173. }
  174. }
  175. return $lines;
  176. }
  177. /**
  178. * Removes trailing newlines from a line of text. This is meant to be used
  179. * with array_walk().
  180. *
  181. * @param string $line The line to trim.
  182. * @param integer $key The index of the line in the array. Not used.
  183. */
  184. static public function trimNewlines(&$line, $key)
  185. {
  186. $line = str_replace(["\n", "\r"], '', $line);
  187. }
  188. /**
  189. * Checks a diff for validity.
  190. *
  191. * This is here only for debugging purposes.
  192. */
  193. protected function _check($from_lines, $to_lines)
  194. {
  195. if (serialize($from_lines) != serialize($this->getOriginal())) {
  196. trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
  197. }
  198. if (serialize($to_lines) != serialize($this->getFinal())) {
  199. trigger_error("Reconstructed final doesn't match", E_USER_ERROR);
  200. }
  201. $rev = $this->reverse();
  202. if (serialize($to_lines) != serialize($rev->getOriginal())) {
  203. trigger_error("Reversed original doesn't match", E_USER_ERROR);
  204. }
  205. if (serialize($from_lines) != serialize($rev->getFinal())) {
  206. trigger_error("Reversed final doesn't match", E_USER_ERROR);
  207. }
  208. $prevtype = null;
  209. foreach ($this->_edits as $edit) {
  210. if ($prevtype == get_class($edit)) {
  211. trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
  212. }
  213. $prevtype = get_class($edit);
  214. }
  215. return true;
  216. }
  217. }