Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
SvnImport
0.00% covered (danger)
0.00%
0 / 88
0.00% covered (danger)
0.00%
0 / 3
600
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 importRepo
0.00% covered (danger)
0.00%
0 / 66
0.00% covered (danger)
0.00%
0 / 1
272
1<?php
2
3use MediaWiki\Extension\CodeReview\Backend\CodeRepository;
4use MediaWiki\Extension\CodeReview\Backend\CodeRevision;
5use MediaWiki\Extension\CodeReview\Backend\SubversionAdaptor;
6use MediaWiki\MediaWikiServices;
7
8$IP = getenv( 'MW_INSTALL_PATH' );
9if ( $IP === false ) {
10    $IP = __DIR__ . '/../../..';
11}
12require_once "$IP/maintenance/Maintenance.php";
13
14class SvnImport extends Maintenance {
15
16    public function __construct() {
17        parent::__construct();
18        $this->addDescription( 'Import revisions to Code Review from a Subversion repo' );
19        $this->addOption( 'precache', 'Pre-cache diffs for last N revisions. ' .
20            'May be a positive integer, 0 (for none) or \'all\'.  Default is 0', false, true );
21        $this->addArg( 'repo', 'The name of the repo. Use \'all\' to import from all defined repos' );
22        $this->addArg( 'start', 'The revision to begin the import from. If not specified then ' .
23            'it starts from the last repo imported to the wiki. Ignored if ' .
24            "'all' is specified for <repo>", false );
25
26        $this->requireExtension( 'CodeReview' );
27    }
28
29    public function execute() {
30        $cacheSize = 0;
31        if ( $this->hasOption( 'precache' ) ) {
32            $cacheSize = $this->getOption( 'precache' );
33            if ( strtolower( $cacheSize ) !== 'all' ) {
34                if ( preg_match( '/^\d+$/', $cacheSize ) ) {
35                    $cacheSize = intval( $cacheSize );
36                } else {
37                    $this->fatalError( "Invalid argument for --precache (must be a positive integer," .
38                        " 0 or 'all')" );
39                }
40            }
41        }
42
43        $repo = $this->getArg( 0 );
44
45        if ( $repo == 'all' ) {
46            $repoList = CodeRepository::getRepoList();
47            /**
48             * @var $repoInfo CodeRepository
49             */
50            foreach ( $repoList as $repoInfo ) {
51                $this->importRepo( $repoInfo->getName(), null, $cacheSize );
52            }
53        } else {
54            $startRev = null;
55            if ( $this->hasArg( 1 ) ) {
56                $startRev = $this->getArg( 1 );
57            }
58            $this->importRepo( $repo, $startRev, $cacheSize );
59        }
60    }
61
62    /**
63     * Import a repository in the local database.
64     * @param string $repoName Local name of repository
65     * @param int|null $start Revision to begin the import from
66     *   (Default: null, means last stored revision);
67     * @param int $cacheSize
68     * @return void
69     */
70    private function importRepo( $repoName, $start = null, $cacheSize = 0 ) {
71        global $wgCodeReviewImportBatchSize;
72
73        static $adaptorReported = false;
74
75        $repo = CodeRepository::newFromName( $repoName );
76
77        if ( !$repo ) {
78            $this->error( "Invalid repo $repoName" );
79            return;
80        }
81
82        $svn = SubversionAdaptor::newFromRepo( $repo->getPath() );
83        if ( !$adaptorReported ) {
84            $this->output( 'Using ' . get_class( $svn ) . " adaptor\n" );
85            $adaptorReported = true;
86        }
87
88        $this->output( "IMPORT FROM REPO: $repoName\n" );
89        $lastStoredRev = $repo->getLastStoredRev();
90        $this->output( "Last stored revision: $lastStoredRev\n" );
91
92        $chunkSize = $wgCodeReviewImportBatchSize;
93
94        $startTime = microtime( true );
95        $revCount = 0;
96        $start = ( $start !== null ) ? intval( $start ) : $lastStoredRev + 1;
97
98        /*
99         * FIXME: when importing only a part of a repository, the given path
100         * might not have been created with revision 1. For example, the
101         * mediawiki '/trunk/phase3' got created with r1284.
102         */
103        if ( $start > ( $lastStoredRev + 1 ) ) {
104            $this->error( "Invalid starting point. r{$start} is beyond last stored revision: r" .
105                ( $lastStoredRev + 1 ) );
106            return;
107        }
108
109        $this->output( "Syncing from r$start to HEAD...\n" );
110
111        if ( !$svn->canConnect() ) {
112            $this->error( "Unable to connect to repository." );
113            return;
114        }
115
116        $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
117
118        while ( true ) {
119            $log = $svn->getLog( '', $start, $start + $chunkSize - 1 );
120            if ( empty( $log ) ) {
121                # Repo seems to give a blank when max rev is invalid, which
122                # stops new revisions from being added. Try to avoid this
123                # by trying less at a time from the last point.
124                if ( $chunkSize <= 1 ) {
125                    // done!
126                    break;
127                }
128                $chunkSize = max( 1, floor( $chunkSize / 4 ) );
129                continue;
130            } else {
131                $start += $chunkSize;
132            }
133            if ( !is_array( $log ) ) {
134                // @TODO: cleanup :)
135                var_dump( $log );
136                $this->fatalError( 'Log entry is not an array! See content above.' );
137            }
138            foreach ( $log as $data ) {
139                $revCount++;
140                $delta = microtime( true ) - $startTime;
141                $revSpeed = $revCount / $delta;
142
143                $codeRev = CodeRevision::newFromSvn( $repo, $data );
144                $codeRev->save();
145
146                $this->output( sprintf( "%d %s %s (%0.1f revs/sec)\n",
147                    $codeRev->getId(),
148                    wfTimestamp( TS_DB, $codeRev->getTimestamp() ),
149                    $codeRev->getAuthor(),
150                    $revSpeed ) );
151            }
152            $lbFactory->waitForReplication( [ 'ifWritesSince' => 5 ] );
153        }
154
155        if ( $cacheSize !== 0 ) {
156            $dbw = wfGetDB( DB_PRIMARY );
157            $options = [ 'ORDER BY' => 'cr_id DESC' ];
158
159            if ( $cacheSize == 'all' ) {
160                $this->output( "Pre-caching all uncached diffs...\n" );
161            } else {
162                if ( $cacheSize == 1 ) {
163                    $this->output( "Pre-caching the latest diff...\n" );
164                } else {
165                    $this->output( "Pre-caching the latest $cacheSize diffs...\n" );
166                }
167                $options['LIMIT'] = $cacheSize;
168            }
169
170            // Get all rows for this repository that don't already have a diff filled in.
171            // This is LIMITed according to the $cacheSize setting, above, so only the
172            // rows that we plan to pre-cache are returned.
173            // TODO: This was optimised in order to skip rows that already have a diff,
174            // which is mostly what is required, but there may be situations where
175            // you want to re-calculate diffs (e.g. if $wgCodeReviewMaxDiffPaths
176            // changes). If these situations arise we will either want to revert
177            // this behavior, or add a --force flag or something.
178            $res = $dbw->select(
179                'code_rev',
180                'cr_id',
181                [ 'cr_repo_id' => $repo->getId(), 'cr_diff IS NULL OR cr_diff = ""' ],
182                __METHOD__,
183                $options
184            );
185            foreach ( $res as $row ) {
186                $repo->getRevision( $row->cr_id );
187                // trigger caching
188                $diff = $repo->getDiff( $row->cr_id );
189                $msg = "Diff r{$row->cr_id} ";
190                if ( is_int( $diff ) ) {
191                    $msg .= 'Skipped: ' . CodeRepository::getDiffErrorMessage( $diff );
192                } else {
193                    $msg .= 'done';
194                }
195                $this->output( $msg . "\n" );
196            }
197        } else {
198            $this->output( "Pre-caching skipped.\n" );
199        }
200        $this->output( "Done!\n" );
201    }
202}
203
204$maintClass = SvnImport::class;
205require_once RUN_MAINTENANCE_IF_MAIN;