1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
4 // Imports data from a trac sqlite database
7 'description' => 'content',
8 'type' => 'classification',
9 'estimatedhours' => 'estimated',
10 'ec_branches' => 'branches',
11 'ec_features' => 'features',
14 $trac_wiki_names = array(
15 'TracAccessibility' => true,
18 'TracBrowser' => true,
20 'TracChangeset' => true,
21 'TracEnvironment' => true,
22 'TracFastCgi' => true,
26 'TracInstall' => true,
27 'TracInstallPlatforms' => true,
28 'TracInterfaceCustomization' => true,
30 'TracLogging' => true,
31 'TracModPython' => true,
32 'TracMultipleProjects' => true,
33 'TracNotification' => true,
34 'TracPermissions' => true,
35 'TracPlugins' => true,
37 'TracReports' => true,
38 'TracRevisionLog' => true,
39 'TracRoadmap' => 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,
51 'WikiDeletePage' => true,
52 'WikiFormatting' => true,
55 'WikiNewPage' => true,
56 'WikiPageNames' => true,
57 'WikiProcessors' => true,
58 'WikiRestructuredText' => true,
59 'WikiRestructuredTextLinks' => true,
61 'InterMapTxt' => true,
64 'RecentChanges' => true,
69 function trac_date($unix) {
70 return MTrackDB::unixtime($unix);
73 function trac_get_comp($name, $deleted = true)
76 global $components_by_name;
78 if (!strlen($name)) return null;
80 $comp = $components_by_name[$name];
82 /* no longer exists */
83 $comp = new MTrackComponent;
85 $comp->deleted = $deleted;
87 $components_by_name[$comp->name] = $comp;
92 function trac_assoc_comp_and_proj(MTrackComponent $comp, MTrackProject $proj)
94 static $comp_assoc = array();
96 if (isset($comp_assoc[$proj->shortname][$comp->name])) {
100 MTrackDB::q('insert into components_by_project (projid, compid)
101 values (?, ?)', $proj->projid, $comp->compid);
103 $comp_assoc[$proj->shortname][$comp->name] = true;
106 function trac_add_user($username)
108 static $users = array();
111 $username = trim($username);
112 $username = strtolower($username);
114 while (isset($CANON_USERS[$username])) {
115 $username = strtolower($CANON_USERS[$username]);
118 if (preg_match('/[ ,]/', $username)) {
119 // invalid: attempted to set multiple people.
120 // take the first one
121 list($username) = preg_split('/[ ,]+/', $username);
123 while (isset($CANON_USERS[$username])) {
124 $username = strtolower($CANON_USERS[$username]);
128 if (preg_match('/^\d+(\.\d+)?$/', $username)) {
129 // invalid (looks like a version number)
133 if ($username == 'somebody' || $username == '') {
137 if (isset($users[$username])) {
141 $users[$username] = true;
151 'insert into userinfo (userid, active) values (?, ?)',
153 } catch (Exception $e) {
159 function trac_get_milestone($name, MTrackProject $proj)
162 global $milestone_by_name;
163 static $alias = array();
165 $lname = strtolower($name);
166 if (isset($alias[$proj->shortname][$lname])) {
167 $name = $alias[$proj->shortname][$lname];
169 $alias[$proj->shortname][$lname] = $name;
172 $ms = $milestone_by_name[$lname];
174 /* first see if there's a milestone with this name in another project */
175 $ms = MTrackMilestone::loadByName($name);
177 $alias[$proj->shortname][$lname] .= " ($proj->shortname)";
178 $name = $alias[$proj->shortname][$lname];
181 $ms = new MTrackMilestone();
184 $ms->description = '';
186 $milestone_by_name[$lname] = $ms;
191 function trac_get_keyword($word)
193 static $words = array();
195 if (isset($words[$word])) {
196 return $words[$word];
199 $kw = MTrackKeyword::loadByWord($word);
203 $kw = new MTrackKeyword;
204 $kw->keyword = $word;
213 function progress($msg)
217 static $clr_eol = null;
218 static $clr_eod = null;
220 if ($clr_eol === null) {
224 $clr_eol = shell_exec("tput el");
225 $clr_eod = shell_exec("tput ed");
232 if ($events % 10 || $now - $last > 2) {
233 echo "\r$clr_eod$msg"; flush();
238 $components_by_name = array();
240 function adjust_links($reason, $ticket_prefix, MTrackProject $project)
242 return $project->adjust_links($reason, $ticket_prefix);
245 function import_from_trac(MTrackProject $project, $import_from_db, $ticket_prefix = false)
247 global $components_by_name;
248 global $milestone_by_name;
250 echo "Importing trac database $import_from_db\n"; flush();
252 $start_import = time();
254 /* reset this list so that we can detect conflicting names
256 $milestone_by_name = array();
259 if (!file_exists("$import_from_db/db/trac.db")) {
260 echo "No such file $import_from_db/db/trac.db\n";
264 $trac = new PDO('sqlite:' . $import_from_db . "/db/trac.db");
265 $trac->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
267 //date_default_timezone_set('UTC');
269 $CS = MTrackChangeset::begin('~import~', "Import trac from $import_from_db");
271 foreach ($trac->query(
272 "select type, name, value from enum")->fetchAll()
275 if ($row['type'] == 'priority') {
277 $pri = MTrackPriority::loadByName($row['name']);
278 } catch (Exception $e) {
279 $pri = new MTrackPriority;
280 $pri->name = $row['name'];
281 $pri->value = $row['value'];
286 if ($row['type'] == 'severity') {
288 $pri = MTrackSeverity::loadByName($row['name']);
289 } catch (Exception $e) {
290 $pri = new MTrackSeverity;
291 $pri->name = $row['name'];
292 $pri->value = $row['value'];
297 if ($row['type'] == 'resolution') {
299 $pri = MTrackResolution::loadByName($row['name']);
300 } catch (Exception $e) {
301 $pri = new MTrackResolution;
302 $pri->name = $row['name'];
303 $pri->value = $row['value'];
308 if ($row['type'] == 'ticket_type') {
310 $pri = MTrackClassification::loadByName($row['name']);
311 } catch (Exception $e) {
312 $pri = new MTrackClassification;
313 $pri->name = $row['name'];
314 $pri->value = $row['value'];
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);
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);
331 $name .= ' (' . $project->shortname . ')';
333 $ms = new MTrackMilestone();
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])];
340 $ms->pmid = $pms->mid;
343 $ms->description = $row['description'];
344 $ms->duedate = trac_date($row['due']);
345 $ms->completed = trac_date($row['completed']);
347 $milestone_by_name[strtolower($row['name'])] = $ms;
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);
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) {
362 $row['reporter'] = trac_add_user($row['reporter']);
363 progress("issue $row[id] $row[reporter]");
365 $fields = array('summary', 'description', 'resolution', 'status',
366 'owner', 'summary', 'component', 'priority', 'severity',
368 'version', 'cc', 'keywords', 'milestone', 'reporter', 'type');
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'];
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'])) {
390 $row[$field] = $hist['oldvalue'];
395 $ctime = (int)$row['time'];
397 MTrackAuth::su($row['reporter']);
398 $CS = MTrackChangeset::begin('ticket:X', $row['summary'], $ctime);
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'];
410 $issue->addEffort(0, $row['estimatedhours']);
411 $issue->addEffort($row['totalhours']);
413 if (strlen($row['component'])) {
414 $comp = trac_get_comp($row['component']);
415 $issue->assocComponent($comp);
417 if (strlen($row['milestone'])) {
418 $ms = trac_get_milestone($row['milestone'], $project);
419 $issue->assocMilestone($ms);
422 foreach (array('keywords', 'features', 'ec_features',
424 'branches', 'ec_branches') as $field) {
425 foreach (preg_split("/\s+/", $row[$field]) as $w) {
427 $kw = trac_get_keyword($w);
428 $issue->assocKeyword($kw);
433 if (strlen($row['owner']) && $row['owner'] != 'somebody') {
434 $row['owner'] = trac_add_user($row['owner']);
435 $issue->owner = $row['owner'];
438 if ($ticket_prefix) {
439 $issue->nsident = $project->shortname . $row['id'];
441 $issue->nsident = $row['id'];
446 # if ($issue->tid != $row['id']) {
447 # throw new Exception(
448 # "expected doc to be created with $row[id], got $issue->tid");
450 $CS->setObject("ticket:" . $issue->tid);
457 /* now make a pass through the history to flesh out the comments and
459 * This can use up a surprising amount of memory, so we stage in
462 echo "\nLooking for changes in $import_from_db\n"; flush();
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);
469 foreach ($changes as $i => $row) {
470 // we order by field because we always want "estimatedhours"
471 // to apply before "hours"
473 "select * from ticket_change
474 where time = ? and ticket = ? and author = ?
478 $batch = $q->fetchAll(PDO::FETCH_ASSOC);
479 if (empty($batch)) continue;
480 list($first) = $batch;
483 $first['author'] = trac_add_user($first['author']);
484 MTrackAuth::su($first['author']);
486 progress("issue $first[ticket] changed by $first[author]");
488 if ($ticket_prefix) {
489 $issue = MTrackIssue::loadByNSIdent(
490 $project->shortname . $first['ticket']);
492 $issue = MTrackIssue::loadByNSIdent($first['ticket']);
495 $CS = MTrackChangeset::begin("ticket:" . $issue->tid,
496 "changed", $first['time']);
499 foreach ($batch as $row) {
500 switch ($row['field']) {
502 $row['newvalue'] = adjust_links($row['newvalue'], $ticket_prefix, $project);
503 $issue->addComment($row['newvalue']);
504 $CS->setReason($row['newvalue']);
508 $row['newvalue'] = trac_add_user($row['newvalue']);
509 if ($row['newvalue'] == 'somebody') {
510 $issue->owner = null;
512 $issue->owner = $row['newvalue'];
517 if ($row['newvalue'] == 'closed') {
520 $issue->status = $row['newvalue'];
525 $issue->description = adjust_links($row['newvalue'],
526 $ticket_prefix, $project);
535 $name = $row['field'];
536 $issue->$name = $row['newvalue'];
540 foreach ($issue->getComponents() as $comp) {
541 $comp = trac_get_comp($comp);
543 $issue->dissocComponent($comp);
546 if (strlen($row['newvalue'])) {
547 $comp = trac_get_comp($row['newvalue']);
548 $issue->assocComponent($comp);
553 foreach ($issue->getMilestones() as $ms) {
554 $ms = trac_get_milestone($ms, $project);
556 $issue->dissocMilestone($ms);
559 if (strlen($row['newvalue'])) {
560 $ms = trac_get_milestone($row['newvalue'], $project);
561 $issue->assocMilestone($ms);
571 foreach ($issue->getKeywords() as $w) {
572 $kw = trac_get_keyword($w);
573 $issue->dissocKeyword($kw);
575 foreach (preg_split("/\s+/", $row['newvalue']) as $w) {
577 $kw = trac_get_keyword($w);
578 $issue->assocKeyword($kw);
584 $issue->classification = $row['newvalue'];
593 $issue->addEffort($row['newvalue'] + 0);
596 case 'estimatedhours':
597 $issue->addEffort(0, $row['newvalue'] + 0);
601 throw new Exception("cant handle field $row[field]");
609 } catch (Exception $e) {
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) {
622 MTrackAuth::su($row['author']);
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]");
628 if ($ticket_prefix) {
629 $issue = MTrackIssue::loadByNSIdent(
630 $project->shortname . $row['id']);
632 $issue = MTrackIssue::loadByNSIdent($row['id']);
635 $CS = MTrackChangeset::begin("ticket:" . $issue->tid,
636 $row['description'], $row['time']);
638 $afile = $import_from_db . "/attachments/ticket/$row[id]/";
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']) {
649 MTrackAttachment::add("ticket:$issue->tid",
650 $afile, $row['filename'], $CS);
653 } catch (Exception $e) {
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) {
667 $fields = array('summary',
669 'resolution', 'status',
670 'owner', 'summary', 'component', 'priority', 'severity',
672 'version', 'cc', 'keywords', 'milestone', 'reporter', 'type');
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);
683 $row[$field] = $custom['value'];
688 if ($ticket_prefix) {
689 $issue = MTrackIssue::loadByNSIdent($project->shortname . $row['id']);
691 $issue = MTrackIssue::loadByNSIdent($row['id']);
695 $row['owner'] = trac_add_user($row['owner']);
701 'classification' => 'type',
706 foreach ($fmap as $sname => $fname) {
707 if (is_int($sname) || ctype_digit($sname)) {
710 if ($fname == 'description') {
711 $row[$fname] = adjust_links($row[$fname], $ticket_prefix, $project);
713 if ($issue->$sname != $row[$fname]) {
715 $issue->$sname = $row[$fname];
719 $comp = reset($issue->getComponents());
720 if ($comp != $row['component']) {
722 $issue->dissocComponent(trac_get_comp($comp));
723 if (strlen($row['component'])) {
724 $comp = trac_get_comp($row['component']);
725 $issue->assocComponent($comp);
729 $ms = reset($issue->getMilestones());
730 if ($ms != $row['milestone']) {
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);
740 progress("$row[id] fixup");
741 if ($issue->updated) {
742 $last_cs = MTrackChangeset::get($issue->updated);
744 $last_cs = MTrackChangeset::get($issue->created);
747 "The importer detected manual database changes; " .
748 "revising ticket to match");
749 $CS = MTrackChangeset::begin("ticket:" . $issue->tid,
751 strtotime($last_cs->when));
758 echo "\nProcessing wiki pages\n"; flush();
760 /* wiki, jungle is posse */
761 global $trac_wiki_names;
764 $wiki_page_remap = array();
765 $suf = MTrackConfig::get('core', 'wikifilenamesuffix');
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
771 $all_wiki_page_names = array();
772 foreach ($trac->query(
773 "select distinct name from wiki")->fetchAll(PDO::FETCH_COLUMN, 0)
775 $all_wiki_page_names[$name] = $name;
778 foreach ($all_wiki_page_names as $name) {
779 $elements = explode('/', $name);
780 if (count($elements) > 1) {
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';
790 throw new Exception("wiki collision between $n and $name");
796 echo "The following pages will be renamed\n";
797 print_r($wiki_page_remap);
800 foreach ($trac->query(
801 "SELECT * from wiki order by time, name, version")
802 ->fetchAll(PDO::FETCH_ASSOC) as $row) {
804 if (isset($trac_wiki_names[$row['name']])) {
807 if (isset($wiki_page_remap[$row['name']])) {
808 $row['name'] = $wiki_page_remap[$row['name']];
811 $author = trac_add_user($row['author']);
813 MTrackAuth::su($author);
814 $row['author'] = $author;
815 } catch (Exception $e) {
816 echo "Error while assuming $author ($row[author])\n";
820 if ($ticket_prefix) {
821 $row['name'] = $project->shortname . '/' . $row['name'];
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']);
829 $wiki = new MTrackWikiItem($row['name']);
831 progress("$row[name] $row[version]");
832 $wiki->content = adjust_links($row['text'], $ticket_prefix, $project);
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) {
843 MTrackAuth::su($row['author']);
845 $row['author'] = trac_add_user($row['author']);
846 $row['filename'] = trac_attachment_name($row['filename']);
848 progress("wiki $row[id] attachment $row[filename] $row[author]");
850 if ($ticket_prefix) {
851 $name = $project->shortname . '/' . $row['id'];
856 $wiki = MTrackWikiItem::loadByPageName($name);
862 $CS = MTrackChangeset::begin('wiki:' . $name,
863 $row['description'], $row['time']);
865 $afile = $import_from_db . "/attachments/wiki/$row[id]/";
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']) {
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/*");
882 echo trac_attachment_name($f), "\n";
884 throw new Exception("fail");
886 MTrackAttachment::add("wiki:$name",
887 $afile, $row['filename'], $CS);
890 } catch (Exception $e) {
898 $end_import = time();
899 $elapsed = $end_import - $start_import;
900 echo "\nDone with $import_from_db (in $elapsed seconds)\n"; flush();
903 function trac_attachment_name($name)
905 $name = urldecode($name);
906 $name = str_replace('+', ' ', $name);