import
[web.mtrack] / bin / import-trac.php
1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
3
4 // Imports data from a trac sqlite database
5
6 $name_map = array(
7     'description' => 'content',
8     'type' => 'classification',
9     'estimatedhours' => 'estimated',
10     'ec_branches' => 'branches',
11     'ec_features' => 'features',
12 );
13
14 $trac_wiki_names = array(
15   'TracAccessibility' => true,
16   'TracAdmin' => true,
17   'TracBackup' => true,
18   'TracBrowser' => true,
19   'TracCgi' => true,
20   'TracChangeset' => true,
21   'TracEnvironment' => true,
22   'TracFastCgi' => true,
23   'TracGuide' => true,
24   'TracImport' => true,
25   'TracIni' => true,
26   'TracInstall' => true,
27   'TracInstallPlatforms' => true,
28   'TracInterfaceCustomization' => true,
29   'TracLinks' => true,
30   'TracLogging' => true,
31   'TracModPython' => true,
32   'TracMultipleProjects' => true,
33   'TracNotification' => true,
34   'TracPermissions' => true,
35   'TracPlugins' => true,
36   'TracQuery' => true,
37   'TracReports' => true,
38   'TracRevisionLog' => true,
39   'TracRoadmap' => true,
40   'TracRss' => true,
41   'TracSearch' => true,
42   'TracStandalone' => true,
43   'TracSupport' => true,
44   'TracSyntaxColoring' => true,
45   'TracTickets' => true,
46   'TracTicketsCustomFields' => true,
47   'TracTimeline' => true,
48   'TracUnicode' => true,
49   'TracUpgrade' => true,
50   'TracWiki' => true,
51   'WikiDeletePage' => true,
52   'WikiFormatting' => true,
53   'WikiHtml' => true,
54   'WikiMacros' => true,
55   'WikiNewPage' => true,
56   'WikiPageNames' => true,
57   'WikiProcessors' => true,
58   'WikiRestructuredText' => true,
59   'WikiRestructuredTextLinks' => true,
60   'CamelCase' => true,
61   'InterMapTxt' => true,
62   'InterTrac' => true,
63   'InterWiki' => true,
64   'RecentChanges' => true,
65   'SandBox' => true,
66   'TitleIndex' => true,
67 );
68
69 function trac_date($unix) {
70   return MTrackDB::unixtime($unix);
71 }
72
73 function trac_get_comp($name, $deleted = true)
74 {
75   global $CS;
76   global $components_by_name;
77   
78   if (!strlen($name)) return null;
79
80   $comp = $components_by_name[$name];
81   if ($comp === null) {
82     /* no longer exists */
83     $comp = new MTrackComponent;
84     $comp->name = $name;
85     $comp->deleted = $deleted;
86     $comp->save($CS);
87     $components_by_name[$comp->name] = $comp;
88   }
89   return $comp;
90 }
91
92 function trac_assoc_comp_and_proj(MTrackComponent $comp, MTrackProject $proj)
93 {
94   static $comp_assoc = array();
95
96   if (isset($comp_assoc[$proj->shortname][$comp->name])) {
97     return;
98   }
99
100   MTrackDB::q('insert into components_by_project (projid, compid)
101     values (?, ?)', $proj->projid, $comp->compid);
102
103   $comp_assoc[$proj->shortname][$comp->name] = true;
104 }
105
106 function trac_add_user($username)
107 {
108   static $users = array();
109   global $CANON_USERS;
110   
111   $username = trim($username);
112   $username = strtolower($username);
113
114   while (isset($CANON_USERS[$username])) {
115     $username = strtolower($CANON_USERS[$username]);
116   }
117
118   if (preg_match('/[ ,]/', $username)) {
119     // invalid: attempted to set multiple people.
120     // take the first one
121     list($username) = preg_split('/[ ,]+/', $username);
122
123     while (isset($CANON_USERS[$username])) {
124       $username = strtolower($CANON_USERS[$username]);
125     }
126   }
127
128   if (preg_match('/^\d+(\.\d+)?$/', $username)) {
129     // invalid (looks like a version number)
130     return null;
131   }
132
133   if ($username == 'somebody' || $username == '') {
134     return null;
135   }
136
137   if (isset($users[$username])) {
138     return $username;
139   }
140
141   $users[$username] = true;
142   switch ($username) {
143     case 'trac':
144       $active = 0;
145       break;
146     default:
147       $active = 1;
148   }
149   try {
150     MTrackDB::q(
151     'insert into userinfo (userid, active) values (?, ?)',
152     $username, $active);
153   } catch (Exception $e) {
154   }
155
156   return $username;
157 }
158
159 function trac_get_milestone($name, MTrackProject $proj)
160 {
161   global $CS;
162   global $milestone_by_name;
163   static $alias = array();
164
165   $lname = strtolower($name);
166   if (isset($alias[$proj->shortname][$lname])) {
167     $name = $alias[$proj->shortname][$lname];
168   } else {
169     $alias[$proj->shortname][$lname] = $name;
170   }
171
172   $ms = $milestone_by_name[$lname];
173   if ($ms === null) {
174     /* first see if there's a milestone with this name in another project */
175     $ms = MTrackMilestone::loadByName($name);
176     if ($ms) {
177       $alias[$proj->shortname][$lname] .= " ($proj->shortname)";
178       $name = $alias[$proj->shortname][$lname];
179     }
180       
181     $ms = new MTrackMilestone();
182     $ms->name = $name;
183     $ms->deleted = true;
184     $ms->description = '';
185     $ms->save($CS);
186     $milestone_by_name[$lname] = $ms;
187   }
188   return $ms;
189 }
190
191 function trac_get_keyword($word)
192 {
193   static $words = array();
194
195   if (isset($words[$word])) {
196     return $words[$word];
197   }
198
199   $kw = MTrackKeyword::loadByWord($word);
200
201   if (!$kw) {
202     global $CS;
203     $kw = new MTrackKeyword;
204     $kw->keyword = $word;
205     $kw->save($CS);
206   }
207
208   $words[$word] = $kw;
209
210   return $kw;
211 }
212
213 function progress($msg)
214 {
215   static $events = 0;
216   static $last = 0;
217   static $clr_eol = null;
218   static $clr_eod = null;
219
220   if ($clr_eol === null) {
221     /* el: clr_eol
222      * ed: clr_eos
223      */
224     $clr_eol = shell_exec("tput el");
225     $clr_eod = shell_exec("tput ed");
226   }
227
228   $events++;
229
230   $now = time();
231
232   if ($events % 10 || $now - $last > 2) {
233     echo "\r$clr_eod$msg"; flush();
234   }
235   $last = $now;
236 }
237   
238 $components_by_name = array();
239
240 function adjust_links($reason, $ticket_prefix, MTrackProject $project)
241 {
242   return $project->adjust_links($reason, $ticket_prefix);
243 }
244
245 function import_from_trac(MTrackProject $project, $import_from_db, $ticket_prefix = false)
246 {
247   global $components_by_name;
248   global $milestone_by_name;
249
250   echo "Importing trac database $import_from_db\n"; flush();
251
252   $start_import = time();
253
254   /* reset this list so that we can detect conflicting names
255    * across projects */
256   $milestone_by_name = array();
257
258
259   if (!file_exists("$import_from_db/db/trac.db")) {
260     echo "No such file $import_from_db/db/trac.db\n";
261     exit(1);
262   }
263
264   $trac = new PDO('sqlite:' . $import_from_db . "/db/trac.db");
265   $trac->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
266
267   //date_default_timezone_set('UTC');
268
269   $CS = MTrackChangeset::begin('~import~', "Import trac from $import_from_db");
270
271   foreach ($trac->query(
272         "select type, name, value from enum")->fetchAll()
273       as $row) {
274
275     if ($row['type'] == 'priority') {
276       try {
277         $pri = MTrackPriority::loadByName($row['name']);
278       } catch (Exception $e) {
279         $pri = new MTrackPriority;
280         $pri->name = $row['name'];
281         $pri->value = $row['value'];
282         $pri->save($CS);
283       }
284     }
285
286     if ($row['type'] == 'severity') {
287       try {
288         $pri = MTrackSeverity::loadByName($row['name']);
289       } catch (Exception $e) {
290         $pri = new MTrackSeverity;
291         $pri->name = $row['name'];
292         $pri->value = $row['value'];
293         $pri->save($CS);
294       }
295     }
296
297     if ($row['type'] == 'resolution') {
298       try {
299         $pri = MTrackResolution::loadByName($row['name']);
300       } catch (Exception $e) {
301         $pri = new MTrackResolution;
302         $pri->name = $row['name'];
303         $pri->value = $row['value'];
304         $pri->save($CS);
305       }
306     }
307
308     if ($row['type'] == 'ticket_type') {
309       try {
310         $pri = MTrackClassification::loadByName($row['name']);
311       } catch (Exception $e) {
312         $pri = new MTrackClassification;
313         $pri->name = $row['name'];
314         $pri->value = $row['value'];
315         $pri->save($CS);
316       }
317     }
318   }
319
320   foreach ($trac->query('select name from component')->fetchAll() as $row) {
321     $comp = trac_get_comp($row['name'], false);
322     trac_assoc_comp_and_proj($comp, $project);
323   }
324
325   foreach ($trac->query("SELECT * from milestone order by name")
326       ->fetchAll(PDO::FETCH_ASSOC) as $row) {
327     /* first see if there's a milestone with this name in another project */
328     $name = $row['name'];
329     $ms = MTrackMilestone::loadByName($name);
330     if ($ms) {
331       $name .= ' (' . $project->shortname . ')';
332     }
333     $ms = new MTrackMilestone();
334     $ms->name = $name;
335     /* for names of the form: sprint.1 sprint.2, tie them back as
336      * children of "sprint" */
337     if (preg_match("/^(.*)\.(\d+)$/", $name, $M)) {
338       $pms = $milestone_by_name[strtolower($M[1])];
339       if ($pms !== null) {
340         $ms->pmid = $pms->mid;
341       }
342     }
343     $ms->description = $row['description'];
344     $ms->duedate = trac_date($row['due']);
345     $ms->completed = trac_date($row['completed']);
346     $ms->save($CS);
347     $milestone_by_name[strtolower($row['name'])] = $ms;
348   }
349
350   $CS->commit();
351   $CS = null;
352
353   list($maxtkt) = $trac->query("select max(id) from ticket")->fetchAll(PDO::FETCH_COLUMN, 0);
354   MTrackConfig::append('trac_import',
355     "max_ticket:$project->shortname", $maxtkt);
356
357   /* first pass is to reserve ticket ids that match the trac db */
358   foreach ($trac->query(
359         "SELECT * from ticket order by id")
360       ->fetchAll(PDO::FETCH_ASSOC) as $row) {
361     
362     $row['reporter'] = trac_add_user($row['reporter']);
363     progress("issue $row[id] $row[reporter]");
364
365     $fields = array('summary', 'description', 'resolution', 'status',
366         'owner', 'summary', 'component', 'priority', 'severity',
367         'changelog',
368         'version', 'cc', 'keywords', 'milestone', 'reporter', 'type');
369
370     foreach ($trac->query(
371           "select name, value from ticket_custom where ticket='$row[id]'")
372         ->fetchAll(PDO::FETCH_ASSOC) as $custom) {
373       if (strlen($custom['value'])) {
374         $field = $custom['name'];
375         $row[$field] = $custom['value'];
376         $fields[] = $field;
377       }
378     }
379
380     /* take a peek at the change history on the ticket to see if we can
381      * determine the original field values */
382     foreach ($fields as $field) {
383       foreach ($trac->query(
384             "SELECT oldvalue from ticket_change where ticket = '" .
385             $row['id'] . "' and field='$field' order by time LIMIT 1")
386           ->fetchAll(PDO::FETCH_ASSOC) as $hist) {
387         if (!strlen($hist['oldvalue'])) {
388           $row[$field] = null;
389         } else {
390           $row[$field] = $hist['oldvalue'];
391         }
392       }
393     }
394
395     $ctime = (int)$row['time'];
396
397     MTrackAuth::su($row['reporter']);
398     $CS = MTrackChangeset::begin('ticket:X', $row['summary'], $ctime);
399
400     $issue = new MTrackIssue();
401     $issue->summary = $row['summary'];
402     $issue->description = adjust_links($row['description'], $ticket_prefix, $project);
403     $issue->priority = $row['priority'];
404     $issue->classification = $row['type'];
405     $issue->resolution = $row['resolution'];
406     $issue->severity = $row['severity'];
407     $issue->changelog = $row['changelog'];
408     $issue->cc = $row['cc'];
409
410     $issue->addEffort(0, $row['estimatedhours']);
411     $issue->addEffort($row['totalhours']);
412
413     if (strlen($row['component'])) {
414       $comp = trac_get_comp($row['component']);
415       $issue->assocComponent($comp);
416     }
417     if (strlen($row['milestone'])) {
418       $ms = trac_get_milestone($row['milestone'], $project);
419       $issue->assocMilestone($ms);
420     }
421
422     foreach (array('keywords', 'features', 'ec_features',
423           'version',
424           'branches', 'ec_branches') as $field) {
425       foreach (preg_split("/\s+/", $row[$field]) as $w) {
426         if (strlen($w)) {
427           $kw = trac_get_keyword($w);
428           $issue->assocKeyword($kw);
429         }
430       }
431     }
432
433     if (strlen($row['owner']) && $row['owner'] != 'somebody') {
434       $row['owner'] = trac_add_user($row['owner']);
435       $issue->owner = $row['owner'];
436     }
437
438     if ($ticket_prefix) {
439       $issue->nsident = $project->shortname . $row['id'];
440     } else {
441       $issue->nsident = $row['id'];
442     }
443
444     $issue->save($CS);
445
446 #    if ($issue->tid != $row['id']) {
447 #      throw new Exception(
448 #          "expected doc to be created with $row[id], got $issue->tid");
449 #    }
450     $CS->setObject("ticket:" . $issue->tid);
451     $CS->commit();
452     $CS = null;
453     $issue = null;
454     MTrackAuth::drop();
455   }
456
457   /* now make a pass through the history to flesh out the comments and
458    * other changes.
459    * This can use up a surprising amount of memory, so we stage in
460    * the work. */
461
462   echo "\nLooking for changes in $import_from_db\n"; flush();
463
464   $changes = $trac->query(
465       "select distinct time, ticket, author from 
466       ticket_change order by ticket asc, time, author")
467     ->fetchAll(PDO::FETCH_NUM);
468
469   foreach ($changes as $i => $row) {
470     // we order by field because we always want "estimatedhours"
471     // to apply before "hours"
472     $q = $trac->prepare(
473         "select * from ticket_change
474         where time = ? and ticket = ? and author = ?
475         order by field
476         ");
477     $q->execute($row);
478     $batch = $q->fetchAll(PDO::FETCH_ASSOC);
479     if (empty($batch)) continue;
480     list($first) = $batch;
481     global $CS;
482
483     $first['author'] = trac_add_user($first['author']);
484     MTrackAuth::su($first['author']);
485     try {
486       progress("issue $first[ticket] changed by $first[author]");
487
488       if ($ticket_prefix) {
489         $issue = MTrackIssue::loadByNSIdent(
490                   $project->shortname . $first['ticket']);
491       } else {
492         $issue = MTrackIssue::loadByNSIdent($first['ticket']);
493       }
494
495       $CS = MTrackChangeset::begin("ticket:" . $issue->tid,
496           "changed", $first['time']);
497
498
499       foreach ($batch as $row) {
500         switch ($row['field']) {
501           case 'comment':
502             $row['newvalue'] = adjust_links($row['newvalue'], $ticket_prefix, $project);
503             $issue->addComment($row['newvalue']);
504             $CS->setReason($row['newvalue']);
505             break;
506
507           case 'owner':
508             $row['newvalue'] = trac_add_user($row['newvalue']);
509             if ($row['newvalue'] == 'somebody') {
510               $issue->owner = null;
511             } else {
512               $issue->owner = $row['newvalue'];
513             }
514             break;
515
516           case 'status':
517             if ($row['newvalue'] == 'closed') {
518               $issue->close();
519             } else {
520               $issue->status = $row['newvalue'];
521             }
522             break;
523
524           case 'description':
525             $issue->description = adjust_links($row['newvalue'],
526                                     $ticket_prefix, $project);
527             break;
528
529           case 'resolution':
530           case 'summary':
531           case 'priority':
532           case 'severity':
533           case 'changelog':
534           case 'cc':
535             $name = $row['field'];
536             $issue->$name = $row['newvalue'];
537             break;
538
539           case 'component':
540             foreach ($issue->getComponents() as $comp) {
541               $comp = trac_get_comp($comp);
542               if ($comp) {
543                 $issue->dissocComponent($comp);
544               }
545             }
546             if (strlen($row['newvalue'])) {
547               $comp = trac_get_comp($row['newvalue']);
548               $issue->assocComponent($comp);
549             }
550             break;
551
552           case 'milestone':
553             foreach ($issue->getMilestones() as $ms) {
554               $ms = trac_get_milestone($ms, $project);
555               if ($ms) {
556                 $issue->dissocMilestone($ms);
557               }
558             }
559             if (strlen($row['newvalue'])) {
560               $ms = trac_get_milestone($row['newvalue'], $project);
561               $issue->assocMilestone($ms);
562             }
563             break;
564
565           case 'keywords':
566           case 'features':
567           case 'ec_features':
568           case 'ec_branches':
569           case 'branches':
570           case 'version':
571             foreach ($issue->getKeywords() as $w) {
572               $kw = trac_get_keyword($w);
573               $issue->dissocKeyword($kw);
574             }
575             foreach (preg_split("/\s+/", $row['newvalue']) as $w) {
576               if (strlen($w)) {
577                 $kw = trac_get_keyword($w);
578                 $issue->assocKeyword($kw);
579               }
580             }
581             break;
582
583           case 'type':
584             $issue->classification = $row['newvalue'];
585             break;
586
587           case 'totalhours':
588           case 'reporter':
589             /* ignore */
590             break;
591
592           case 'hours':
593             $issue->addEffort($row['newvalue'] + 0);
594             break;
595
596           case 'estimatedhours':
597             $issue->addEffort(0, $row['newvalue'] + 0);
598             break;
599
600           default:
601             throw new Exception("cant handle field $row[field]");
602         }
603       }
604       $issue->save($CS); 
605       $issue = null;
606       $CS->commit();
607       $CS = null;
608
609     } catch (Exception $e) {
610       MTrackAuth::drop();
611       throw $e;
612     }
613     MTrackAuth::drop();
614   }
615
616   /* Find attachments */
617   foreach ($trac->query(
618       "select id, filename, size, time, description, author
619         from attachment where type = 'ticket'")
620       ->fetchAll(PDO::FETCH_ASSOC) as $row) {
621
622     MTrackAuth::su($row['author']);
623     try {
624       $row['author'] = trac_add_user($row['author']);
625       $row['filename'] = trac_attachment_name($row['filename']);
626       progress("issue $row[id] attachment $row[filename] $row[author]");
627
628       if ($ticket_prefix) {
629         $issue = MTrackIssue::loadByNSIdent(
630             $project->shortname . $row['id']);
631       } else {
632         $issue = MTrackIssue::loadByNSIdent($row['id']);
633       }
634
635       $CS = MTrackChangeset::begin("ticket:" . $issue->tid,
636           $row['description'], $row['time']);
637
638       $afile = $import_from_db . "/attachments/ticket/$row[id]/";
639
640       // trac uses weird url encoding on the filename on disk.
641       // this weird looking code is because I'm too lazy to reverse
642       // engineer their encoding
643       foreach (glob("$afile/*") as $potential) {
644         if (trac_attachment_name(basename($potential)) == $row['filename']) {
645           $afile = $potential;
646           break;
647         }
648       }
649       MTrackAttachment::add("ticket:$issue->tid",
650           $afile, $row['filename'], $CS);
651       $CS->commit();
652
653     } catch (Exception $e) {
654       MTrackAuth::drop();
655       throw $e;
656     }
657     MTrackAuth::drop();
658   }
659
660   /* Make another pass over the tickets to catch changes made to the
661    * database by hand that are not journalled in the trac change tables */
662   MTrackAuth::su('trac');
663   foreach ($trac->query(
664         "SELECT * from ticket order by id")
665       ->fetchAll(PDO::FETCH_ASSOC) as $row) {
666
667     $fields = array('summary', 
668         'description',
669         'resolution', 'status',
670         'owner', 'summary', 'component', 'priority', 'severity',
671         'changelog',
672         'version', 'cc', 'keywords', 'milestone', 'reporter', 'type');
673
674     foreach ($trac->query(
675           "select name, value from ticket_custom where ticket=$row[id]")
676         ->fetchAll(PDO::FETCH_ASSOC) as $custom) {
677       if (strlen($custom['value'])) {
678         $field = $custom['name'];
679         if ($field == 'description') {
680           $custom['value'] = adjust_links($custom['value'], $ticket_prefix, $project);
681         }
682
683         $row[$field] = $custom['value'];
684         $fields[] = $field;
685       }
686     }
687
688     if ($ticket_prefix) {
689       $issue = MTrackIssue::loadByNSIdent($project->shortname . $row['id']);
690     } else {
691       $issue = MTrackIssue::loadByNSIdent($row['id']);
692     }
693     $needed = false;
694
695     $row['owner'] = trac_add_user($row['owner']);
696     $fmap = array(
697       'summary',
698       'description',
699       'priority',
700       'status',
701       'classification' => 'type',
702       'resolution',
703       'owner',
704       'severity');
705
706     foreach ($fmap as $sname => $fname) {
707       if (is_int($sname) || ctype_digit($sname)) {
708         $sname = $fname;
709       }
710       if ($fname == 'description') {
711         $row[$fname] = adjust_links($row[$fname], $ticket_prefix, $project);
712       }
713       if ($issue->$sname != $row[$fname]) {
714         $needed = true;
715         $issue->$sname = $row[$fname];
716       }
717     }
718
719     $comp = reset($issue->getComponents());
720     if ($comp != $row['component']) {
721       $needed = true;
722       $issue->dissocComponent(trac_get_comp($comp));
723       if (strlen($row['component'])) {
724         $comp = trac_get_comp($row['component']);
725         $issue->assocComponent($comp);
726       }
727     }
728
729     $ms = reset($issue->getMilestones());
730     if ($ms != $row['milestone']) {
731       $needed = true;
732       $issue->dissocMilestone(trac_get_milestone($ms, $project));
733       if (strlen($row['milestone'])) {
734         $ms = trac_get_milestone($row['milestone'], $project);
735         $issue->assocMilestone($ms);
736       }
737     }
738
739     if ($needed) {
740       progress("$row[id] fixup");
741       if ($issue->updated) {
742         $last_cs = MTrackChangeset::get($issue->updated);
743       } else {
744         $last_cs = MTrackChangeset::get($issue->created);
745       }
746       $issue->addComment(
747         "The importer detected manual database changes; " .
748         "revising ticket to match");
749       $CS = MTrackChangeset::begin("ticket:" . $issue->tid,
750             "fixup", 
751             strtotime($last_cs->when));
752       $issue->save($CS);
753       $CS->commit();
754     }
755   }
756   MTrackAuth::drop();
757
758   echo "\nProcessing wiki pages\n"; flush();
759
760   /* wiki, jungle is posse */
761   global $trac_wiki_names;
762   $wiki = null;
763
764   $wiki_page_remap = array();
765   $suf = MTrackConfig::get('core', 'wikifilenamesuffix');
766   if (!strlen($suf)) {
767     /* Here's a fun problem; trac allows both pages and dirs to exist with the
768      * same name (because its dirs aren't really dirs, they're just illusions)
769      * We need to notice those that are pages and that collide with dirs and
770      * rename them */
771     $all_wiki_page_names = array();
772     foreach ($trac->query(
773           "select distinct name from wiki")->fetchAll(PDO::FETCH_COLUMN, 0)
774         as $name) {
775       $all_wiki_page_names[$name] = $name;
776     }
777
778     foreach ($all_wiki_page_names as $name) {
779       $elements = explode('/', $name);
780       if (count($elements) > 1) {
781         $accum = array();
782         while (count($elements) > 1) {
783           $accum[] = array_shift($elements);
784           $n = join('/', $accum);
785           if (isset($all_wiki_page_names[$n])) {
786             // Collision; try adding a suffix of "Page"
787             if (!isset($all_wiki_page_names[$n . 'Page'])) {
788               $wiki_page_remap[$n] = $n . 'Page';
789             } else {
790               throw new Exception("wiki collision between $n and $name");
791             }
792           }
793         }
794       }
795     }
796     echo "The following pages will be renamed\n";
797     print_r($wiki_page_remap);
798   }
799
800   foreach ($trac->query(
801         "SELECT * from wiki order by time, name, version")
802       ->fetchAll(PDO::FETCH_ASSOC) as $row) {
803
804     if (isset($trac_wiki_names[$row['name']])) {
805       continue;
806     }
807     if (isset($wiki_page_remap[$row['name']])) {
808       $row['name'] = $wiki_page_remap[$row['name']];
809     }
810
811     $author = trac_add_user($row['author']);
812     try {
813       MTrackAuth::su($author);
814       $row['author'] = $author;
815     } catch (Exception $e) {
816       echo "Error while assuming $author ($row[author])\n";
817       MTrackAuth::drop();
818       throw $e;
819     }
820     if ($ticket_prefix) {
821       $row['name'] = $project->shortname . '/' . $row['name'];
822     }
823     $CS = MTrackChangeset::begin('wiki:' . $row['name'],
824         $row['comment'], $row['time']);
825     if (!is_object($wiki) || $wiki->pagename != $row['name']) {
826       $wiki = MTrackWikiItem::loadByPageName($row['name']);
827     }
828     if (!$wiki) {
829       $wiki = new MTrackWikiItem($row['name']);
830     }
831     progress("$row[name] $row[version]");
832     $wiki->content = adjust_links($row['text'], $ticket_prefix, $project);
833     $wiki->save($CS);
834     $CS->commit();
835     MTrackAuth::drop();
836   }
837   /* Find attachments */
838   foreach ($trac->query(
839       "select id, filename, size, time, description, author
840         from attachment where type = 'wiki'")
841       ->fetchAll(PDO::FETCH_ASSOC) as $row) {
842
843     MTrackAuth::su($row['author']);
844     try {
845       $row['author'] = trac_add_user($row['author']);
846       $row['filename'] = trac_attachment_name($row['filename']);
847
848       progress("wiki $row[id] attachment $row[filename] $row[author]");
849
850       if ($ticket_prefix) {
851         $name = $project->shortname . '/' . $row['id'];
852       } else {
853         $name = $row['id'];
854       }
855
856       $wiki = MTrackWikiItem::loadByPageName($name);
857       if (!$wiki) {
858         MTrackAuth::drop();
859         continue;
860       }
861
862       $CS = MTrackChangeset::begin('wiki:' . $name,
863           $row['description'], $row['time']);
864
865       $afile = $import_from_db . "/attachments/wiki/$row[id]/";
866
867       // trac uses weird url encoding on the filename on disk.
868       // this weird looking code is because I'm too lazy to reverse
869       // engineer their encoding
870       foreach (glob("$afile/*") as $potential) {
871         if (trac_attachment_name(basename($potential)) == $row['filename']) {
872           $afile = $potential;
873           break;
874         }
875       }
876       if (!is_file($afile)) {
877         echo "Looking for attachment $row[filename]\n";
878         echo "Didn't find it in $afile\n";
879         $g = glob("$afile/*");
880         print_r($g);
881         foreach ($g as $f) {
882           echo trac_attachment_name($f), "\n";
883         }
884         throw new Exception("fail");
885       }
886       MTrackAttachment::add("wiki:$name",
887           $afile, $row['filename'], $CS);
888       $CS->commit();
889
890     } catch (Exception $e) {
891       MTrackAuth::drop();
892       throw $e;
893     }
894     MTrackAuth::drop();
895   }
896
897
898   $end_import = time();
899   $elapsed = $end_import - $start_import;
900   echo "\nDone with $import_from_db (in $elapsed seconds)\n"; flush();
901 }
902
903 function trac_attachment_name($name)
904 {
905   $name = urldecode($name);
906   $name = str_replace('+', ' ', $name);
907   return $name;
908 }