/[drupal]/contributions/modules/versioncontrol_svn/logparser.inc
ViewVC logotype

Contents of /contributions/modules/versioncontrol_svn/logparser.inc

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.2 - (show annotations) (download) (as text)
Sun May 10 02:05:10 2009 UTC (6 months, 2 weeks ago) by sdboyer
Branch: MAIN
CVS Tags: HEAD
Changes since 1.1: +42 -67 lines
File MIME type: text/x-php
Latest round of work; implemented speed and memory-optimizing breakup of the single aggregateInfo request, simplified and sorted out some more things on the deletion heuristics, plus other minor items.
1 <?php
2
3 class VersioncontrolSvnLogHandler implements CLIParser, Iterator {
4 /**
5 *
6 * @var SvnInfo
7 */
8 public $aggregateInfo;
9 /**
10 *
11 * @var SvnInstance
12 */
13 protected $instance;
14
15 public $revisions = array();
16 protected $revision;
17
18 // protected $outputHandles = array();
19
20 /**
21 *
22 * @var SvnInfo
23 */
24 protected $opRevInfo;
25 protected $opRevAggregator = array();
26 protected $log, $info;
27
28 protected $allActions = array();
29 protected $actions;
30
31 protected $firstRev, $lastRev, $currentRev;
32 protected $ready = FALSE;
33
34 public function __construct(SvnInstance $instance, $first_rev, $last_rev) {
35 $this->instance = $instance;
36 $this->firstRev = $first_rev;
37 $this->lastRev = $last_rev;
38 $this->log = new VersioncontrolSvnLogParser();
39 }
40
41 public function openOutputHandle() {
42 return $this->log->openOutputHandle();
43 }
44
45 /**
46 * Implementation of CLIParser::parseOutput().
47 *
48 * This method is called once, on the firing of the SvnLog command's output
49 * parser during execution. We iterate over the contents of the log output and
50 * determine what additional information we'll need from svn, queue all those
51 * requests up into a command, then fire them as needed either at the end,
52 * or during iteration.
53 *
54 * @return VersioncontrolSvnLogHandler
55 */
56 public function parseOutput() {
57 $this->log->parseOutput();
58 // Outer revisions loop; each iteration deals encomapsses a single revision.
59 foreach ($this->log as $revision) {
60 // Inner revisions loop; each iteration encompasses with a single path on
61 // the current revision.
62 $this->aggregateInfo[$revision['rev']] = $this->instance->svn('info');
63 $this->info = $this->aggregateInfo[$revision['rev']];
64 foreach ($revision['paths'] as $path => $path_info) {
65 // Add info requests for copyfrom and delfrom info, which will be used
66 // later to fill out item metadata.
67 foreach (array('copyfrom', 'delfrom') as $from) {
68 if (isset($path_info[$from])) {
69 $this->info->target($path_info[$from]['path'], $path_info[$from]['rev'], TRUE);
70 }
71 }
72
73 // By bundling many URL detail retrievals in one `svn info` call, we
74 // can keep the number of 'svn info' invocations down to a minimum.
75 // Doing this one by one would be a major blow to performance.
76 if ($path_info['action'] & VERSIONCONTROL_SVN_ACTION_CHANGE) {
77 $this->info->target($path, $revision['rev'], TRUE);
78 if ($path_info['action'] == VERSIONCONTROL_SVN_ACTION_MODIFY) {
79 // Modified items require the special operative/peg rev syntax to
80 // svn info, so we queue them into their own special per-rev object.
81 $this->queueOpRevTarget($path, $revision['rev'], $revision['rev'] - 1);
82 }
83 }
84 if ($path_info['action'] == VERSIONCONTROL_SVN_ACTION_DELETE_SIMPLE) {
85 // Ugly delete actions do not originate in the previous revision, so
86 // querying there would result in an error. Only simple deletes here
87 $this->info->target($path, $revision['rev'] - 1, TRUE);
88 }
89 } // end inner revisions loop
90 } // end outer revisions loop
91 // // All queued up - fire it and store the resulting info parser.
92 // $this->info = $this->aggregateInfo->execute();
93 // // Kill aggregateinfo to free memory - it can easily contain LOTS of objects
94 // unset($this->aggregateInfo);
95 $this->ready = TRUE;
96 return $this;
97 }
98
99 protected function queueOpRevTarget($target, $peg_rev, $op_rev) {
100 if (empty($this->opRevAggregator[$peg_rev])) {
101 $this->opRevAggregator[$peg_rev] = $this->instance->svn('info');
102 $this->opRevInfo = $this->opRevAggregator[$peg_rev];
103 $this->opRevInfo->revision($op_rev);
104 }
105 $this->opRevInfo->target($target, $peg_rev, TRUE);
106 }
107
108 /**
109 * Implementation of CLIParser::clear(). Empty because this parser can't be
110 * cleared.
111 */
112 public function clear() {}
113
114 /**
115 * Implementation of Iterator::rewind().
116 */
117 public function rewind() {
118 if (!$this->ready) {
119 throw new Exception('The parser has not yet collected all the necessary data, and is not prepared for iteration.', E_RECOVERABLE_ERROR);
120 }
121 $this->currentRev = $this->firstRev;
122 }
123
124 /**
125 * Implementation of Iterator::key().
126 */
127 public function key() {
128 return $this->currentRev;
129 }
130
131 /**
132 * Implementation of Iterator::valid().
133 */
134 public function valid() {
135 return $this->ready ? $this->currentRev <= $this->lastRev : FALSE;
136 }
137
138 /**
139 * Implementation of Iterator::next().
140 */
141 public function next() {
142 $this->opRevAggregator[$this->currentRev] = NULL;
143 $this->aggregateInfo[$this->currentRev] = NULL;
144 // unset($this->revision);
145 $this->currentRev++;
146 }
147
148 protected function pointersToCurrentRev() {
149 // Retrieve the revision data from the cached xml output of svn log.
150 $this->revision = $this->log->seek($this->currentRev);
151 $this->info = $this->aggregateInfo[$this->currentRev]->execute();
152 $this->actions = &$this->revision['paths'];
153 $this->opRevInfo = &$this->opRevAggregator[$this->currentRev];
154 if ($this->opRevInfo instanceof SvnInfo) {
155 $this->opRevInfo->clear(SvnCommand::PRESERVE_ALL);
156 }
157 }
158
159 /**
160 * Implementation of Iterator::current().
161 *
162 * Builds the array versioncontrol_svn wants on the fly, using the stored
163 * seekable log and info iterators, as well as any necessary stored opRev
164 * requests.
165 */
166 public function current() {
167 $this->pointersToCurrentRev();
168 // Retrieve all info items for the current revision.
169 foreach ($this->info->seekRev($this->currentRev) as $path => $info_item) {
170 // $path = $info_item['path']; // REFACTORED OUT
171 if (empty($this->actions[$path])) {
172 continue; // means we're on one that was called up by somethin else
173 }
174 $this->actions[$path]['current_item'] = self::itemFromInfo($info_item);
175
176 if (isset($this->actions[$path]['copyfrom'])) { // can happen for 'A' or 'R' actions
177 // Yay, we can have the source item without invoking the binary.
178 $this->actions[$path]['source_item'] = $this->actions[$path]['current_item'];
179 $this->actions[$path]['source_item']['path'] = $this->actions[$path]['copyfrom']['path'];
180 $this->actions[$path]['source_item']['rev'] = $this->actions[$path]['copyfrom']['rev'];
181 unset($this->actions[$path]['copyfrom']); // not needed anymore
182 }
183 }
184
185 // Now we retrieve all source items of 'M' actions from our aggregated
186 // output handler. Mind that these can have different paths than the current
187 // item.
188 if (!empty($this->opRevInfo) && $this->opRevInfo instanceof SvnInfo) {
189 foreach ($this->opRevInfo->execute() as $info_item) {
190 $this->actions[$info_item['path']]['source_item'] = self::itemFromInfo($info_item);
191 }
192 }
193
194 // Bonus feature: Recognize moves and copies by inspecting the source item
195 // of added items and matching it to a deleted one. We can it this way
196 // because if the icon really were an added one then it wouldn't have
197 // a source item at all. The nice thing is that we can even get rid
198 // of more 'svn info' invocations by removing the corresponding 'D' actions.
199 foreach ($this->actions as &$action) {
200 // TODO check and make sure this is right - encomapsses both A and R
201 // FIXME this section needs work. More of it needs to be resolved back in
202 // the xml parsing stage.
203 if ($action['action'] & VERSIONCONTROL_SVN_ACTION_CHANGE && isset($action['source_item'])) {
204 // Search through all other items if they contain the source item
205 // of this add action.
206 foreach ($this->revision['paths'] as $other_path => $other_path_info) {
207 // Only consider most recent delete actions with a matching path.
208 if ($this->actions[$other_path]['action'] & VERSIONCONTROL_SVN_ACTION_DELETE
209 && $action['source_item']['rev'] == ($action['current_item']['rev'] - 1)
210 && $other_path == $action['source_item']['path']) {
211 // Hah! gotcha. Die, delete action, die! Instead, the "deleted" item
212 // is just going to be the source item of the now merged action.
213 // ...that's right, what we've got here is a move.
214 unset($this->revision['paths'][$other_path]);
215 // unset($this->actions[$other_path]);
216 $action['action'] = VERSIONCONTROL_SVN_ACTION_MOVE;
217 break;
218 }
219 }
220 // If the item was not moved, it must have been copied.
221 // (Otherwise there would be no source item.)
222 if ($action['action'] != VERSIONCONTROL_SVN_ACTION_MOVE) {
223 $action['action'] = VERSIONCONTROL_SVN_ACTION_COPY;
224 }
225 }
226 }
227
228 // Fourth step: retrieve the source items of the given path,
229 // and construct the $action info array for that path.
230 foreach ($this->actions as $path => &$action) {
231 // In case there was a modified item of which the parent was moved
232 // to a new location just in this very revision, it has been missing
233 // from the retrieved modified items further above, which means the
234 // remaining ones could not be mapped to their actions.
235 // In order to map them anyways, we have to retrieve them one by one -
236 // which is slow, but will hardly ever occur anyways.
237 //
238 // REFACTORED *** this should never EVER happen (Can't remember why, now...damn)
239 // if ($actions[$path]['action'] == 'M' && !isset($actions[$path]['source_item'])) {
240 // $source_items = svnlib_info_cached($actions[$path]['url'], $revision['rev'], $revision['rev'] - 1);
241 // if ($source_items) {
242 // $source_item = reset($source_items); // first item
243 // $actions[$path]['source_item'] = self::itemFromInfo($source_item);
244 // }
245 // }
246
247 // Ok, that was the easy part - 'M' and 'A' are covered and should now
248 // have a current item (in all cases) and source item (if it existed).
249 // The hard part is 'D'.
250 if ($action['action'] & VERSIONCONTROL_SVN_ACTION_DELETE) {
251 // If the original type was 'R', we essentially have two actions:
252 // one delete action and one add/move/copy action. We now have the data
253 // for the latter one, but we can also get the data for the delete action
254 // by making the algorithm believe that the action is 'D', not 'A'.
255 // Thus, we store the deleted item parent info into different slot.
256 $into = $action['action'] == VERSIONCONTROL_SVN_ACTION_REPLACE ? 'replaced_item' : 'source_item';
257 if (!empty($action['delfrom'])) {
258 $source_item = $this->info->seekBoth($action['delfrom']['rev'], $this->instance->getRepoRoot() . $action['delfrom']['path']);
259 $action[$into] = self::itemFromInfo($source_item);
260 $action['current_item'] = $action[$into];
261 $action['current_item']['path'] = $path;
262 $action['current_item']['rev'] = $this->currentRev;
263 // type is ok, as it's the same as in the source item.
264 }
265 elseif ($source_item = $this->info->seekBoth($this->currentRev - 1, $this->instance->getRepoRoot() . $path)) {
266 $action[$into] = self::itemFromInfo($source_item);
267 $action['current_item'] = $action[$into];
268 $action['current_item']['rev'] = $this->currentRev; // path and type are ok
269 }
270 elseif ($action['action'] == VERSIONCONTROL_SVN_ACTION_REPLACE) {
271 // This represents a very slim case, we've got the rare special case that
272 // the parent directory has been copied to a new location where the
273 // old copied item was swapped against a new one. (Yeah, happens.)
274 // It is effectively an in-place replace without any parent
275 // information; since there is no no parent/source, we record it as a
276 // simple add.
277 $action['action'] = VERSIONCONTROL_SVN_ACTION_REPLACE_INPLACE;
278 }
279 else {
280 // Throw a warning here so that everything doesn't break, but it's
281 // really clear we've found an edge case.
282 throw new Exception("Hit a very, very weird edge case on some operation with a delete component. Revision $this->currentRev, path $path.", E_WARNING);
283 }
284 } // End machinations for actions with any kind of 'delete' component
285 }
286 // All done! Send that sucker out for processing.
287 return $this->revision;
288 }
289
290 static public function itemFromInfo($info) {
291 return array(
292 'type' => $info['type'],
293 'path' => $info['path'],
294 'rev' => $info['created_rev'],
295 );
296 }
297
298 public function procClose($destruct = FALSE) {
299 if ($destruct) {
300 if (is_object($this->log)) {
301 $this->log->procClose($destruct);
302 }
303 if (is_object($this->info)) {
304 $this->info->procClose($destruct);
305 }
306 }
307 }
308 }
309
310 /**
311 * Very much like the parent class, just primes revisions with some additional
312 * data that's specific to versioncontrol_svn.
313 */
314 class VersioncontrolSvnLogParser extends SvnLogXMLParser {
315 protected $actions = array(
316 'A' => VERSIONCONTROL_SVN_ACTION_ADD,
317 'M' => VERSIONCONTROL_SVN_ACTION_MODIFY,
318 'R' => VERSIONCONTROL_SVN_ACTION_REPLACE,
319 'D' => VERSIONCONTROL_SVN_ACTION_DELETE,
320 );
321
322 protected function parse($rev) {
323 $paths = array();
324 $revision = array(
325 'rev' => intval((string) $rev['revision']),
326 'author' => (string) $rev->author,
327 'msg' => rtrim($rev->msg), // no trailing linebreaks
328 'time_t' => strtotime($rev->date),
329 'paths' => &$paths,
330 );
331
332 if (is_object($rev->paths)) {
333 foreach ($rev->paths->path as $logpath) {
334 $path = array(
335 'path' => (string) $logpath,
336 'action' => $this->actions[(string) $logpath['action']],
337 );
338 if (!empty($logpath['copyfrom-path'])) {
339 $path['copyfrom'] = array(
340 'path' => (string) $logpath['copyfrom-path'],
341 'rev' => (string) $logpath['copyfrom-rev'],
342 );
343 }
344 elseif ($path['action'] == VERSIONCONTROL_SVN_ACTION_DELETE) {
345 // First, do the quick-n-easy check to see if this is a move action.
346 $move = $rev->xpath("paths/path[@copyfrom-path = '$path[path]']");
347 if (!empty($move)) {
348 continue;
349 }
350 // On delete actions, check if this is one of the annoying kind. Run
351 // an xpath query, querying for an init-substring match from all of
352 // the paths in this rev that are type 'A' and have copyfrom info.
353 $parents = $rev->xpath("paths/path[@action = 'A' and @copyfrom-rev and @copyfrom-path != '$path[path]' and starts-with('$path[path]',.)]");
354 if (count($parents) > 1) {
355 throw new Exception("Multiple possible copy+delete parents found for $path[path]@$revision[rev]; cannot currently handle this.", E_ERROR);
356 }
357 else {
358 $add_path = array_shift($parents);
359 if (empty($add_path)) {
360 $path['action'] = VERSIONCONTROL_SVN_ACTION_DELETE_SIMPLE;
361 }
362 elseif (strpos($path['path'], (string) $add_path) !== FALSE) {
363 $path['action'] = VERSIONCONTROL_SVN_ACTION_DELETE_UGLY;
364 $path['delfrom'] = array(
365 'path' => $add_path['copyfrom-path'] . substr($path['path'], strlen($add_path)),
366 'rev' => (string) $add_path['copyfrom-rev'],
367 );
368 }
369 else {
370 throw new Exception('Blargh', E_ERROR);
371 }
372 }
373 }
374 $paths[(string) $logpath] = $path;
375 }
376 }
377 return $revision;
378 }
379 }

  ViewVC Help
Powered by ViewVC 1.1.2