PageRenderTime 45ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/extensions/CodeReview/backend/Subversion.php

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