PageRenderTime 36ms CodeModel.GetById 7ms RepoModel.GetById 0ms app.codeStats 0ms

/extensions/CodeReview/backend/Subversion.php

https://github.com/brion/mediawiki-svn
PHP | 363 lines | 288 code | 40 blank | 35 comment | 36 complexity | fde22a4f4a17ade3d00839c3b5ed94dc MD5 | raw file
  1. <?php
  2. if ( !defined( 'MEDIAWIKI' ) ) die();
  3. abstract class SubversionAdaptor {
  4. protected $mRepo;
  5. public static function newFromRepo( $repo ) {
  6. global $wgSubversionProxy, $wgSubversionProxyTimeout;
  7. if ( $wgSubversionProxy ) {
  8. return new SubversionProxy( $repo, $wgSubversionProxy, $wgSubversionProxyTimeout );
  9. } elseif ( function_exists( 'svn_log' ) ) {
  10. return new SubversionPecl( $repo );
  11. } else {
  12. return new SubversionShell( $repo );
  13. }
  14. }
  15. function __construct( $repo ) {
  16. $this->mRepo = $repo;
  17. }
  18. abstract function canConnect();
  19. abstract function getFile( $path, $rev = null );
  20. abstract function getDiff( $path, $rev1, $rev2 );
  21. abstract function getDirList( $path, $rev = null );
  22. /*
  23. array of array(
  24. 'rev' => 123,
  25. 'author' => 'myname',
  26. 'msg' => 'log message'
  27. 'date' => '8601 date',
  28. 'paths' => array(
  29. array(
  30. 'action' => one of M, A, D, R
  31. 'path' => repo URL of file,
  32. ),
  33. ...
  34. )
  35. */
  36. abstract function getLog( $path, $startRev = null, $endRev = null );
  37. protected function _rev( $rev, $default ) {
  38. if ( $rev === null ) {
  39. return $default;
  40. } else {
  41. return intval( $rev );
  42. }
  43. }
  44. }
  45. /**
  46. * Using the SVN PECL extension...
  47. */
  48. class SubversionPecl extends SubversionAdaptor {
  49. function canConnect() {
  50. // TODO!
  51. return true;
  52. }
  53. function getFile( $path, $rev = null ) {
  54. return svn_cat( $this->mRepo . $path, $rev );
  55. }
  56. function getDiff( $path, $rev1, $rev2 ) {
  57. list( $fout, $ferr ) = svn_diff(
  58. $this->mRepo . $path, $rev1,
  59. $this->mRepo . $path, $rev2 );
  60. if ( $fout ) {
  61. // We have to read out the file descriptors. :P
  62. $out = '';
  63. while ( !feof( $fout ) ) {
  64. $out .= fgets( $fout );
  65. }
  66. fclose( $fout );
  67. fclose( $ferr );
  68. return $out;
  69. } else {
  70. return new MWException( "Diffing error" );
  71. }
  72. }
  73. function getDirList( $path, $rev = null ) {
  74. return svn_ls( $this->mRepo . $path,
  75. $this->_rev( $rev, SVN_REVISION_HEAD ) );
  76. }
  77. function getLog( $path, $startRev = null, $endRev = null ) {
  78. wfSuppressWarnings();
  79. $log = svn_log( $this->mRepo . $path,
  80. $this->_rev( $startRev, SVN_REVISION_INITIAL ),
  81. $this->_rev( $endRev, SVN_REVISION_HEAD ) );
  82. wfRestoreWarnings();
  83. return $log;
  84. }
  85. }
  86. /**
  87. * Using the thingy-bobber
  88. */
  89. class SubversionShell extends SubversionAdaptor {
  90. function canConnect() {
  91. $command = sprintf(
  92. "svn info %s %s",
  93. $this->getExtraArgs(),
  94. wfEscapeShellArg( $this->mRepo ) );
  95. $result = wfShellExec( $command );
  96. if ( $result == "" ) {
  97. return false;
  98. } elseif ( strpos( $result, "No repository found" ) !== false ) {
  99. return false;
  100. } else {
  101. return true;
  102. }
  103. }
  104. function getFile( $path, $rev = null ) {
  105. if ( $rev )
  106. $path .= "@$rev";
  107. $command = sprintf(
  108. "svn cat %s %s",
  109. $this->getExtraArgs(),
  110. wfEscapeShellArg( $this->mRepo . $path ) );
  111. return wfShellExec( $command );
  112. }
  113. function getDiff( $path, $rev1, $rev2 ) {
  114. $command = sprintf(
  115. "svn diff -r%d:%d %s %s",
  116. intval( $rev1 ),
  117. intval( $rev2 ),
  118. $this->getExtraArgs(),
  119. wfEscapeShellArg( $this->mRepo . $path ) );
  120. return wfShellExec( $command );
  121. }
  122. function getLog( $path, $startRev = null, $endRev = null ) {
  123. $lang = wfIsWindows() ? "" : "LC_ALL=en_US.utf-8 ";
  124. $command = sprintf(
  125. "{$lang}svn log -v -r%s:%s %s %s",
  126. wfEscapeShellArg( $this->_rev( $startRev, 'BASE' ) ),
  127. wfEscapeShellArg( $this->_rev( $endRev, 'HEAD' ) ),
  128. $this->getExtraArgs(),
  129. wfEscapeShellArg( $this->mRepo . $path ) );
  130. $lines = explode( "\n", wfShellExec( $command ) );
  131. $out = array();
  132. $divider = str_repeat( '-', 72 );
  133. $formats = array(
  134. 'rev' => '/^r(\d+)$/',
  135. 'author' => '/^(.*)$/',
  136. 'date' => '/^(.*?) \(.*\)$/',
  137. 'lines' => '/^(\d+) lines?$/',
  138. );
  139. $state = "start";
  140. foreach ( $lines as $line ) {
  141. $line = rtrim( $line );
  142. switch( $state ) {
  143. case "start":
  144. if ( $line == $divider ) {
  145. $state = "revdata";
  146. break;
  147. } else {
  148. return $out;
  149. # throw new MWException( "Unexpected start line: $line" );
  150. }
  151. case "revdata":
  152. if ( $line == "" ) {
  153. $state = "done";
  154. break;
  155. }
  156. $data = array();
  157. $bits = explode( " | ", $line );
  158. $i = 0;
  159. foreach ( $formats as $key => $regex ) {
  160. $text = $bits[$i++];
  161. if ( preg_match( $regex, $text, $matches ) ) {
  162. $data[$key] = $matches[1];
  163. } else {
  164. throw new MWException(
  165. "Unexpected format for $key in '$text'" );
  166. }
  167. }
  168. $data['msg'] = '';
  169. $data['paths'] = array();
  170. $state = 'changedpaths';
  171. break;
  172. case "changedpaths":
  173. if ( $line == "Changed paths:" ) { // broken when svn messages are not in English
  174. $state = "path";
  175. } elseif ( $line == "" ) {
  176. // No changed paths?
  177. $state = "msg";
  178. } else {
  179. throw new MWException(
  180. "Expected 'Changed paths:' or '', got '$line'" );
  181. }
  182. break;
  183. case "path":
  184. if ( $line == "" ) {
  185. // Out of paths. Move on to the message...
  186. $state = 'msg';
  187. } else {
  188. if ( preg_match( '/^ (.) (.*)$/', $line, $matches ) ) {
  189. $data['paths'][] = array(
  190. 'action' => $matches[1],
  191. 'path' => $matches[2] );
  192. }
  193. }
  194. break;
  195. case "msg":
  196. $data['msg'] .= $line;
  197. if ( --$data['lines'] ) {
  198. $data['msg'] .= "\n";
  199. } else {
  200. unset( $data['lines'] );
  201. $out[] = $data;
  202. $state = "start";
  203. }
  204. break;
  205. case "done":
  206. throw new MWException( "Unexpected input after end: $line" );
  207. default:
  208. throw new MWException( "Invalid state '$state'" );
  209. }
  210. }
  211. return $out;
  212. }
  213. function getDirList( $path, $rev = null ) {
  214. $command = sprintf(
  215. "svn list --xml -r%s %s %s",
  216. wfEscapeShellArg( $this->_rev( $rev, 'HEAD' ) ),
  217. $this->getExtraArgs(),
  218. wfEscapeShellArg( $this->mRepo . $path ) );
  219. $document = new DOMDocument();
  220. if ( !@$document->loadXML( wfShellExec( $command ) ) )
  221. // svn list --xml returns invalid XML if the file does not exist
  222. // FIXME: report bug upstream
  223. return false;
  224. $entries = $document->getElementsByTagName( 'entry' );
  225. $result = array();
  226. foreach ( $entries as $entry ) {
  227. $item = array();
  228. $item['type'] = $entry->getAttribute( 'kind' );
  229. foreach ( $entry->childNodes as $child ) {
  230. switch ( $child->nodeName ) {
  231. case 'name':
  232. $item['name'] = $child->textContent;
  233. break;
  234. case 'size':
  235. $item['size'] = intval( $child->textContent );
  236. break;
  237. case 'commit':
  238. $item['created_rev'] = intval( $child->getAttribute( 'revision' ) );
  239. foreach ( $child->childNodes as $commitEntry ) {
  240. switch ( $commitEntry->nodeName ) {
  241. case 'author':
  242. $item['last_author'] = $commitEntry->textContent;
  243. break;
  244. case 'date':
  245. $item['time_t'] = wfTimestamp( TS_UNIX, $commitEntry->textContent );
  246. break;
  247. }
  248. }
  249. break;
  250. }
  251. }
  252. $result[] = $item;
  253. }
  254. return $result;
  255. }
  256. /**
  257. * Returns a string of extra arguments to be passed into the shell commands
  258. */
  259. private function getExtraArgs() {
  260. global $wgSubversionOptions, $wgSubversionUser, $wgSubversionPassword;
  261. $args = $wgSubversionOptions;
  262. if ( $wgSubversionUser ) {
  263. $args .= ' --username ' . wfEscapeShellArg( $wgSubversionUser )
  264. . ' --password ' . wfEscapeShellArg( $wgSubversionPassword );
  265. }
  266. return $args;
  267. }
  268. }
  269. /**
  270. * Using a remote JSON proxy
  271. */
  272. class SubversionProxy extends SubversionAdaptor {
  273. function __construct( $repo, $proxy, $timeout = 30 ) {
  274. parent::__construct( $repo );
  275. $this->mProxy = $proxy;
  276. $this->mTimeout = $timeout;
  277. }
  278. function canConnect() {
  279. // TODO!
  280. return true;
  281. }
  282. function getFile( $path, $rev = null ) {
  283. throw new MWException( "NYI" );
  284. }
  285. function getDiff( $path, $rev1, $rev2 ) {
  286. return $this->_proxy( array(
  287. 'action' => 'diff',
  288. 'base' => $this->mRepo,
  289. 'path' => $path,
  290. 'rev1' => $rev1,
  291. 'rev2' => $rev2 ) );
  292. }
  293. function getLog( $path, $startRev = null, $endRev = null ) {
  294. return $this->_proxy( array(
  295. 'action' => 'log',
  296. 'base' => $this->mRepo,
  297. 'path' => $path,
  298. 'start' => $startRev,
  299. 'end' => $endRev ) );
  300. }
  301. function getDirList( $path, $rev = null ) {
  302. return $this->_proxy( array(
  303. 'action' => 'list',
  304. 'base' => $this->mRepo,
  305. 'path' => $path,
  306. 'rev' => $rev ) );
  307. }
  308. protected function _proxy( $params ) {
  309. foreach ( $params as $key => $val ) {
  310. if ( is_null( $val ) ) {
  311. // Don't pass nulls to remote
  312. unset( $params[$key] );
  313. }
  314. }
  315. $target = $this->mProxy . '?' . wfArrayToCgi( $params );
  316. $blob = Http::get( $target, $this->mTimeout );
  317. if ( $blob === false ) {
  318. throw new MWException( "SVN proxy error" );
  319. }
  320. $data = unserialize( $blob );
  321. return $data;
  322. }
  323. }