├── README ├── trac2github.cfg └── trac2github.php /README: -------------------------------------------------------------------------------- 1 | Features: 2 | - Converts milestones/tickets/comments/labels 3 | - Converts Trac usernames to Github usernames 4 | - Sets assignees if possible, sets you an assignee otherwise 5 | - Supports manual splitting of conversion process into stages 6 | - Slows down to match the 60 requests per second API limit rate 7 | 8 | Usage: 9 | 1. Edit the configuration variables in the file above the DO NOT EDIT BELOW comment or use the external configuration file, trac2github.cfg. 10 | 2. Run it from shell, e.g.: 11 | $ php trac2github.php 12 | 13 | Known Problems: 14 | - Strips some characters from ticket bodys / comments like ü/ä/ö etc. -------------------------------------------------------------------------------- /trac2github.cfg: -------------------------------------------------------------------------------- 1 | 'GithubUsername', 19 | 'Trustmaster' => 'trustmaster', 20 | 'John.Done' => 'johndoe' 21 | ); 22 | 23 | //Restrict to certain components (null or Array with components name). 24 | $use_components = null; 25 | //$use_components = array('ios_app'); 26 | 27 | // The PDO driver name to use. 28 | // Options are: 'mysql', 'sqlite', 'pgsql' 29 | $pdo_driver = 'mysql'; 30 | 31 | // MySQL connection info 32 | $mysqlhost_trac = 'Trac MySQL host'; 33 | $mysqluser_trac = 'Trac MySQL user'; 34 | $mysqlpassword_trac = 'Trac MySQL password'; 35 | $mysqldb_trac = 'Trac MySQL database name'; 36 | 37 | // Path to SQLite database file 38 | $sqlite_trac_path = '/path/to/trac.db'; 39 | 40 | // Postgresql connection info 41 | $pgsql_host = 'localhost'; 42 | $pgsql_port = '5432'; 43 | $pgsql_dbname = 'Postgres database name'; 44 | $pgsql_user = 'Postgres user name'; 45 | $pgsql_password = 'Postgres password'; 46 | 47 | // Do not convert milestones at this run 48 | $skip_milestones = false; 49 | 50 | // Do not convert labels at this run 51 | $skip_labels = false; 52 | 53 | $remap_labels = array( 54 | 'T: defect' => 'bug', 55 | 'T: feature' => 'enhancement', 56 | 'T: enhancement' => 'enhancement', 57 | 'R: implemented' => NULL, 58 | 'R: fixed' => NULL, 59 | 'R: invalid' => 'invalid', 60 | 'R: wontfix' => 'wontfix', 61 | 'R: worksforme' => 'worksforme', 62 | 'R: notanissue' => 'notanissue', 63 | ); 64 | 65 | // Do not convert tickets 66 | $skip_tickets = false; 67 | $ticket_offset = 0; // Start at this offset if limit > 0 68 | $ticket_limit = 0; // Max tickets per run if > 0 69 | $ticket_try_preserve_numbers = false; 70 | 71 | // Do not convert comments nor ticket history 72 | $skip_comments = false; 73 | $comments_offset = 0; // Start at this offset if limit > 0 74 | $comments_limit = 0; // Max comments per run if > 0 75 | 76 | // Whether to add a "Migrated-From:" suffix to each issue's body 77 | $add_migrated_suffix = false; 78 | $trac_url = 'http://my.domain/trac/env'; 79 | 80 | // Paths to milestone/ticket cache if you run it multiple times with skip/offset 81 | $save_tickets = './trac_tickets.list'; 82 | 83 | // Set this to true if you want to see the JSON output sent to GitHub 84 | $verbose = false; 85 | 86 | // Uncomment to refresh cache 87 | // @unlink($save_tickets); 88 | 89 | ?> 90 | -------------------------------------------------------------------------------- /trac2github.php: -------------------------------------------------------------------------------- 1 | 'GithubUsername', 21 | 'Trustmaster' => 'trustmaster', 22 | 'John.Done' => 'johndoe' 23 | ); 24 | 25 | //Restrict to certain components (null or Array with components name). 26 | $use_components = null; 27 | 28 | // The PDO driver name to use. 29 | // Options are: 'mysql', 'sqlite', 'pgsql' 30 | $pdo_driver = 'mysql'; 31 | 32 | // MySQL connection info 33 | $mysqlhost_trac = 'Trac MySQL host'; 34 | $mysqluser_trac = 'Trac MySQL user'; 35 | $mysqlpassword_trac = 'Trac MySQL password'; 36 | $mysqldb_trac = 'Trac MySQL database name'; 37 | 38 | // Path to SQLite database file 39 | $sqlite_trac_path = '/path/to/trac.db'; 40 | 41 | // Postgresql connection info 42 | $pgsql_host = 'localhost'; 43 | $pgsql_port = '5432'; 44 | $pgsql_dbname = 'Postgres database name'; 45 | $pgsql_user = 'Postgres user name'; 46 | $pgsql_password = 'Postgres password'; 47 | 48 | // Do not convert milestones at this run 49 | $skip_milestones = false; 50 | 51 | // Do not convert labels at this run 52 | $skip_labels = false; 53 | 54 | $remap_labels = array(); 55 | 56 | // Do not convert tickets 57 | $skip_tickets = false; 58 | $ticket_offset = 0; // Start at this offset if limit > 0 59 | $ticket_limit = 0; // Max tickets per run if > 0 60 | $ticket_try_preserve_numbers = 0; // Try to preserve ticket numbers - create placeholders, error if not match 61 | 62 | // Do not convert comments 63 | $skip_comments = false; 64 | $comments_offset = 0; // Start at this offset if limit > 0 65 | $comments_limit = 0; // Max comments per run if > 0 66 | 67 | // Whether to add a "Migrated-From:" suffix to each issue's body 68 | $add_migrated_suffix = false; 69 | $trac_url = 'http://my.domain/trac/env'; 70 | 71 | // Paths to milestone/ticket cache if you run it multiple times with skip/offset 72 | $save_milestones = '/tmp/trac_milestones.list'; 73 | $save_tickets = '/tmp/trac_tickets.list'; 74 | 75 | // Set this to true if you want to see the JSON output sent to GitHub 76 | $verbose = false; 77 | 78 | $request_count = 0; 79 | 80 | // Uncomment to refresh cache 81 | // @unlink($save_milestones); 82 | // @unlink($save_labels); 83 | // @unlink($save_tickets); 84 | 85 | // DO NOT EDIT BELOW 86 | 87 | if (file_exists('trac2github.cfg')) { 88 | include 'trac2github.cfg'; 89 | } 90 | 91 | error_reporting(E_ALL ^ E_NOTICE); 92 | ini_set('display_errors', 1); 93 | set_time_limit(0); 94 | date_default_timezone_set("UTC"); 95 | 96 | // Connect to Trac database using PDO 97 | switch ($pdo_driver) { 98 | case 'mysql': 99 | $trac_db = new PDO('mysql:host='.$mysqlhost_trac.';dbname='.$mysqldb_trac . ';charset=utf8', $mysqluser_trac, $mysqlpassword_trac); 100 | break; 101 | 102 | case 'sqlite': 103 | // Check the the file exists 104 | if (!file_exists($sqlite_trac_path)) { 105 | echo "SQLITE file does not exist.\n"; 106 | exit; 107 | } 108 | 109 | $trac_db = new PDO('sqlite:'.$sqlite_trac_path); 110 | break; 111 | 112 | case 'pgsql': 113 | $trac_db = new PDO("pgsql:host=$pgsql_host;port=$pgsql_port;dbname=$pgsql_dbname;user=$pgsql_user;password=$pgsql_password"); 114 | break; 115 | 116 | default: 117 | echo "Unknown PDO driver.\n"; 118 | exit; 119 | } 120 | 121 | // Set PDO to throw exceptions on error. 122 | $trac_db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 123 | 124 | echo "Connected to Trac\n"; 125 | 126 | //if restriction to certain components is added, put this in the SQL string 127 | if ($use_components && is_array($use_components)) $my_components = " AND component IN ('".implode("', '", $use_components)."') "; 128 | else $my_components = ""; 129 | 130 | $milestones = array(); 131 | 132 | if (!$skip_milestones) { 133 | // Export all milestones 134 | $res = $trac_db->query("SELECT * FROM milestone ORDER BY due"); 135 | $mnum = 1; 136 | $existing_milestones = array(); 137 | foreach (github_get_milestones() as $m) { 138 | $milestones[crc32(urldecode($m['title']))] = (int)$m['number']; 139 | } 140 | foreach ($res->fetchAll() as $row) { 141 | if (isset($milestones[crc32($row['name'])])) { 142 | echo "Milestone {$row['name']} already exists\n"; 143 | continue; 144 | } 145 | //$milestones[$row['name']] = ++$mnum; 146 | $epochInSecs = (int) ($row['due']/1000000); 147 | echo "due : ".date('Y-m-d\TH:i:s\Z', $epochInSecs)."\n"; 148 | if ($epochInSecs == 0) { 149 | $resp = github_add_milestone(array( 150 | 'title' => $row['name'], 151 | 'state' => $row['completed'] == 0 ? 'open' : 'closed', 152 | 'description' => empty($row['description']) ? '' : translate_markup($row['description']) 153 | )); 154 | } 155 | else { 156 | $resp = github_add_milestone(array( 157 | 'title' => $row['name'], 158 | 'state' => $row['completed'] == 0 ? 'open' : 'closed', 159 | 'description' => empty($row['description']) ? '' : translate_markup($row['description']), 160 | 'due_on' => date('Y-m-d\TH:i:s\Z', $epochInSecs) 161 | )); 162 | } 163 | if (isset($resp['number'])) { 164 | // OK 165 | $milestones[crc32($row['name'])] = (int) $resp['number']; 166 | echo "Milestone {$row['name']} converted to {$resp['number']}\n"; 167 | } else { 168 | // Error 169 | $error = print_r($resp, 1); 170 | echo "Failed to convert milestone {$row['name']}: $error\n"; 171 | } 172 | } 173 | } 174 | 175 | $labels = array(); 176 | $labels['T'] = array(); 177 | $labels['C'] = array(); 178 | $labels['P'] = array(); 179 | $labels['R'] = array(); 180 | 181 | if (!$skip_labels) { 182 | // Export all "labels" 183 | $res = $trac_db->query("SELECT DISTINCT 'T' AS label_type, type AS name, 'cccccc' AS color 184 | FROM ticket WHERE COALESCE(type, '') <> '' 185 | UNION 186 | SELECT DISTINCT 'C' AS label_type, component AS name, '0000aa' AS color 187 | FROM ticket WHERE COALESCE (component, '') <> '' 188 | UNION 189 | SELECT DISTINCT 'P' AS label_type, priority AS name, case when lower(priority) = 'urgent' then 'ff0000' 190 | when lower(priority) = 'high' then 'ff6666' 191 | when lower(priority) = 'medium' then 'ffaaaa' 192 | when lower(priority) = 'low' then 'ffdddd' 193 | when lower(priority) = 'blocker' then 'ffc7f8' 194 | when lower(priority) = 'critical' then 'ffffb8' 195 | when lower(priority) = 'major' then 'f6f6f6' 196 | when lower(priority) = 'minor' then 'dcffff' 197 | when lower(priority) = 'trivial' then 'dce7ff' 198 | else 'aa8888' end color 199 | FROM ticket WHERE COALESCE(priority, '') <> '' 200 | UNION 201 | SELECT DISTINCT 'R' AS label_type, resolution AS name, '55ff55' AS color 202 | FROM ticket WHERE COALESCE(resolution, '') <> ''"); 203 | 204 | $existing_labels = array(); 205 | foreach (github_get_labels() as $l) { 206 | $existing_labels[] = urldecode($l['name']); 207 | } 208 | foreach ($res->fetchAll() as $row) { 209 | $label_name = $row['label_type'] . ': ' . str_replace(",", "", $row['name']); 210 | if (array_key_exists($label_name, $remap_labels)) { 211 | $label_name = $remap_labels[$label_name]; 212 | } 213 | if (empty($label_name)) { 214 | $labels[$row['label_type']][crc32($row['name'])] = NULL; 215 | continue; 216 | } 217 | if (in_array($label_name, $existing_labels)) { 218 | echo "Label {$row['name']} already exists\n"; 219 | $labels[$row['label_type']][crc32($row['name'])] = $label_name; 220 | continue; 221 | } 222 | $resp = github_add_label(array( 223 | 'name' => $label_name, 224 | 'color' => $row['color'] 225 | )); 226 | 227 | if (isset($resp['url'])) { 228 | // OK 229 | $labels[$row['label_type']][crc32($row['name'])] = $resp['name']; 230 | echo "Label {$row['name']} converted to {$resp['name']}\n"; 231 | } else { 232 | // Error 233 | $error = print_r($resp, 1); 234 | echo "Failed to convert label {$row['name']}: $error\n"; 235 | } 236 | } 237 | } 238 | 239 | // Try get previously fetched tickets 240 | $tickets = array(); 241 | if (file_exists($save_tickets)) { 242 | $tickets = unserialize(file_get_contents($save_tickets)); 243 | } 244 | 245 | if (!$skip_tickets) { 246 | // Export tickets 247 | $limit = $ticket_limit > 0 ? "LIMIT $ticket_offset, $ticket_limit" : ''; 248 | 249 | $res = $trac_db->query("SELECT * FROM ticket WHERE 1=1 $my_components ORDER BY id $limit"); 250 | foreach ($res->fetchAll() as $row) { 251 | if (isset($last_ticket_number) and $ticket_try_preserve_numbers) { 252 | if ($last_ticket_number >= $row['id']) { 253 | echo "ERROR: Cannot create ticket #{$row['id']} because issue #{$last_ticket_number} was already created."; 254 | break; 255 | } 256 | while ($last_ticket_number < $row['id']-1) { 257 | $resp = github_add_issue(array( 258 | 'title' => "Placeholder", 259 | 'body' => "This is a placeholder created during migration to preserve original issue numbers.", 260 | 'milestone' => NULL, 261 | 'labels' => array() 262 | )); 263 | if (isset($resp['number'])) { 264 | // OK 265 | $last_ticket_number = $resp['number']; 266 | echo "Created placeholder issue #{$resp['number']}\n"; 267 | $resp = github_update_issue($resp['number'], array( 268 | 'state' => 'closed', 269 | 'labels' => array('invalid'), 270 | )); 271 | if (isset($resp['number'])) { 272 | echo "Closed issue #{$resp['number']}\n"; 273 | } 274 | } 275 | } 276 | } 277 | if (!$skip_comments) { 278 | // restore original values (at ticket creation time), to restore modification history later 279 | foreach (['owner', 'priority', 'resolution', 'milestone', 'type', 'component', 'description', 'summary'] as $f) { 280 | $row[$f] = trac_orig_value($row, $f); 281 | } 282 | } 283 | if (!empty($row['owner']) and !isset($users_list[$row['owner']])) { 284 | $row['owner'] = NULL; 285 | } 286 | $ticketLabels = array(); 287 | if (!empty($labels['T'][crc32($row['type'])])) { 288 | $ticketLabels[] = $labels['T'][crc32($row['type'])]; 289 | } 290 | if (!empty($labels['C'][crc32($row['component'])])) { 291 | $ticketLabels[] = $labels['C'][crc32($row['component'])]; 292 | } 293 | if (!empty($labels['P'][crc32($row['priority'])])) { 294 | $ticketLabels[] = $labels['P'][crc32($row['priority'])]; 295 | } 296 | if (!empty($labels['R'][crc32($row['resolution'])])) { 297 | $ticketLabels[] = $labels['R'][crc32($row['resolution'])]; 298 | } 299 | 300 | $body = make_body($row['description']); 301 | $timestamp = date("j M Y H:i e", $row['time']/1000000); 302 | $body = '**Reported by ' . obfuscate_email($row['reporter']) . ' on ' . $timestamp . "**\n" . $body; 303 | 304 | if (empty($row['milestone'])) { 305 | $milestone = NULL; 306 | } else { 307 | $milestone = $milestones[crc32($row['milestone'])]; 308 | } 309 | if (!empty($row['owner'])) { 310 | $assignee = isset($users_list[$row['owner']]) ? $users_list[$row['owner']] : $row['owner']; 311 | } else { 312 | $assignee = NULL; 313 | } 314 | $resp = github_add_issue(array( 315 | 'title' => $row['summary'], 316 | 'body' => body_with_possible_suffix($body, $row['id']), 317 | 'assignee' => $assignee, 318 | 'milestone' => $milestone, 319 | 'labels' => $ticketLabels 320 | )); 321 | if (isset($resp['number'])) { 322 | // OK 323 | $tickets[$row['id']] = (int) $resp['number']; 324 | $last_ticket_number = $resp['number']; 325 | echo "Ticket #{$row['id']} converted to issue #{$resp['number']}\n"; 326 | if ($ticket_try_preserve_numbers and $row['id'] != $resp['number']) { 327 | echo "ERROR: New ticket number do not match the original one!\n"; 328 | break; 329 | } 330 | if (!$skip_comments) { 331 | if (!add_changes_for_ticket($row['id'], $ticketLabels)) { 332 | break; 333 | } 334 | } else { 335 | if ($row['status'] == 'closed') { 336 | // Close the issue 337 | $resp = github_update_issue($resp['number'], array( 338 | 'state' => 'closed' 339 | )); 340 | if (isset($resp['number'])) { 341 | echo "Closed issue #{$resp['number']}\n"; 342 | } 343 | } 344 | } 345 | 346 | } else { 347 | // Error 348 | $error = print_r($resp, 1); 349 | echo "Failed to convert a ticket #{$row['id']}: $error\n"; 350 | break; 351 | } 352 | } 353 | // Serialize to restore in future 354 | file_put_contents($save_tickets, serialize($tickets)); 355 | } 356 | 357 | echo "Done whatever possible, sorry if not.\n"; 358 | 359 | function trac_orig_value($ticket, $field) { 360 | global $trac_db; 361 | $orig_value = $ticket[$field]; 362 | $res = $trac_db->query("SELECT ticket_change.* FROM ticket_change WHERE ticket = {$ticket['id']} AND field = '$field' ORDER BY time LIMIT 1"); 363 | foreach ($res->fetchAll() as $row) { 364 | $orig_value = $row['oldvalue']; 365 | } 366 | return $orig_value; 367 | } 368 | 369 | function add_changes_for_ticket($ticket, $ticketLabels) { 370 | global $trac_db, $tickets, $labels, $users_list, $milestones, $skip_comments, $verbose; 371 | $res = $trac_db->query("SELECT ticket_change.* FROM ticket_change, ticket WHERE ticket.id = ticket_change.ticket AND ticket = $ticket ORDER BY ticket, time, field <> 'comment'"); 372 | foreach ($res->fetchAll() as $row) { 373 | if ($verbose) print_r($row); 374 | if (!isset($tickets[$row['ticket']])) { 375 | echo "Skipping comment " . $row['time'] . " on unknown ticket " . $row['ticket'] . "\n"; 376 | continue; 377 | } 378 | $timestamp = date("j M Y H:i e", $row['time']/1000000); 379 | if ($row['field'] == 'comment') { 380 | if ($row['newvalue'] != '') { 381 | $text = '**Comment by ' . $row['author'] . ' on ' . $timestamp . "**\n" . $row['newvalue']; 382 | } else { 383 | $text = '**Modified by ' . $row['author'] . ' on ' . $timestamp . "**"; 384 | } 385 | $resp = github_add_comment($tickets[$row['ticket']], translate_markup($text)); 386 | } else if (in_array($row['field'], ['component', 'priority', 'type', 'resolution'])) { 387 | if (in_array($labels[strtoupper($row['field'])[0]][crc32($row['oldvalue'])], $ticketLabels)) { 388 | $index = array_search($labels[strtoupper($row['field'])[0]][crc32($row['oldvalue'])], $ticketLabels); 389 | $ticketLabels[$index] = $labels[strtoupper($row['field'])[0]][crc32($row['newvalue'])]; 390 | } else { 391 | $ticketLabels[] = $labels[strtoupper($row['field'])[0]][crc32($row['newvalue'])]; 392 | } 393 | $resp = github_update_issue($tickets[$ticket], array( 394 | 'labels' => array_values(array_filter($ticketLabels, 'strlen')) 395 | )); 396 | } else if ($row['field'] == 'status') { 397 | $resp = github_update_issue($tickets[$ticket], array( 398 | 'state' => ($row['newvalue'] == 'closed') ? 'closed' : 'open' 399 | )); 400 | } else if ($row['field'] == 'summary') { 401 | $resp = github_update_issue($tickets[$ticket], array( 402 | 'title' => $row['newvalue'] 403 | )); 404 | } else if (false and $row['field'] == 'description') { // TODO? 405 | $body = make_body($row['newvalue']); 406 | $timestamp = date("j M Y H:i e", $row['time']/1000000); 407 | // TODO: 408 | //$body = '**Reported by ' . obfuscate_email($row['reporter']) . ' on ' . $timestamp . "**\n" . $body; 409 | 410 | $resp = github_update_issue($tickets[$ticket], array( 411 | 'body' => $body 412 | )); 413 | } else if ($row['field'] == 'owner') { 414 | if (!empty($row['newvalue'])) { 415 | $assignee = isset($users_list[$row['newvalue']]) ? $users_list[$row['newvalue']] : NULL; 416 | } else { 417 | $assignee = NULL; 418 | } 419 | $resp = github_update_issue($tickets[$ticket], array( 420 | 'assignee' => $assignee 421 | )); 422 | } else if ($row['field'] == 'milestone') { 423 | if (empty($row['newvalue'])) { 424 | $milestone = NULL; 425 | } else { 426 | $milestone = $milestones[crc32($row['newvalue'])]; 427 | } 428 | $resp = github_update_issue($tickets[$ticket], array( 429 | 'milestone' => $milestone 430 | )); 431 | } else { 432 | echo "WARNING: ignoring change of {$row['field']} to {$row['newvalue']}\n"; 433 | continue; 434 | } 435 | if (isset($resp['url'])) { 436 | // OK 437 | echo "Added change {$resp['url']}\n"; 438 | } else { 439 | // Error 440 | $error = print_r($resp, 1); 441 | echo "Failed to add a comment for " . $row['ticket'] . ": $error\n"; 442 | return false; 443 | } 444 | // Wait 1sec to ensure the next event will be after 445 | // just added (apparently github can reorder 446 | // changes/comments if added too fast) 447 | sleep(1); 448 | } 449 | return true; 450 | } 451 | 452 | function github_req($url, $json, $patch = false, $post = true) { 453 | global $username, $password, $request_count; 454 | $ch = curl_init(); 455 | curl_setopt($ch, CURLOPT_USERPWD, "$username:$password"); 456 | curl_setopt($ch, CURLOPT_URL, "https://api.github.com$url"); 457 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 458 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 459 | curl_setopt($ch, CURLOPT_HEADER, true); 460 | curl_setopt($ch, CURLOPT_POST, $post); 461 | curl_setopt($ch, CURLOPT_POSTFIELDS, $json); 462 | curl_setopt($ch, CURLOPT_USERAGENT, "trac2github for $project, admin@example.com"); 463 | if ($patch) { 464 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH'); 465 | } else if ($post) { 466 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); 467 | } else { 468 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET'); 469 | } 470 | $ret = curl_exec($ch); 471 | 472 | $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); 473 | $header = substr($ret, 0, $header_size); 474 | $body = substr($ret, $header_size); 475 | 476 | if ($verbose) print_r($header); 477 | if (!$ret) { 478 | trigger_error(curl_error($ch)); 479 | } 480 | curl_close($ch); 481 | 482 | if ($patch || $post) { 483 | $request_count++; 484 | if($request_count > 50) { 485 | sleep(70); 486 | $request_count = 0; 487 | } 488 | 489 | } 490 | 491 | return $body; 492 | } 493 | 494 | function github_add_milestone($data) { 495 | global $project, $repo, $verbose; 496 | if ($verbose) print_r($data); 497 | return json_decode(github_req("/repos/$project/$repo/milestones", json_encode($data)), true); 498 | } 499 | 500 | function github_add_label($data) { 501 | global $project, $repo, $verbose; 502 | if ($verbose) print_r($data); 503 | return json_decode(github_req("/repos/$project/$repo/labels", json_encode($data)), true); 504 | } 505 | 506 | function github_add_issue($data) { 507 | global $project, $repo, $verbose; 508 | if ($verbose) print_r($data); 509 | return json_decode(github_req("/repos/$project/$repo/issues", json_encode($data)), true); 510 | } 511 | 512 | function github_add_comment($issue, $body) { 513 | global $project, $repo, $verbose; 514 | if ($verbose) print_r($body); 515 | return json_decode(github_req("/repos/$project/$repo/issues/$issue/comments", json_encode(array('body' => $body))), true); 516 | } 517 | 518 | function github_update_issue($issue, $data) { 519 | global $project, $repo, $verbose; 520 | if ($verbose) print_r($data); 521 | return json_decode(github_req("/repos/$project/$repo/issues/$issue", json_encode($data), true), true); 522 | } 523 | 524 | function github_get_milestones() { 525 | global $project, $repo, $verbose; 526 | if ($verbose) print_r($body); 527 | return json_decode(github_req("/repos/$project/$repo/milestones?per_page=100&state=all", false, false, false), true); 528 | } 529 | 530 | function github_get_labels() { 531 | global $project, $repo, $verbose; 532 | if ($verbose) print_r($body); 533 | return array_merge(json_decode(github_req("/repos/$project/$repo/labels?per_page=100", false, false, false), true), json_decode(github_req("/repos/$project/$repo/labels?page=2&per_page=100", false, false, false), true)); 534 | } 535 | 536 | function make_body($description) { 537 | return empty($description) ? 'None' : translate_markup($description); 538 | } 539 | 540 | function translate_markup($data) { 541 | // Replace code blocks with an associated language 542 | $data = preg_replace('/\{\{\{(\s*#!(\w+))?/m', '```$2', $data); 543 | $data = preg_replace('/\}\}\}/', '```', $data); 544 | 545 | // Avoid non-ASCII characters, as that will cause trouble with json_encode() 546 | $data = preg_replace('/[^(\x00-\x7F)]*/','', $data); 547 | 548 | // Translate Trac-style links to Markdown 549 | $data = preg_replace('/\[([^ ]+) ([^\]]+)\]/', '[$2]($1)', $data); 550 | 551 | // Possibly translate other markup as well? 552 | return $data; 553 | } 554 | 555 | function body_with_possible_suffix($body, $id) { 556 | global $add_migrated_suffix, $trac_url; 557 | if (!$add_migrated_suffix) return $body; 558 | return "$body\n\nMigrated-From: $trac_url/ticket/$id"; 559 | } 560 | 561 | function obfuscate_email($text) 562 | { 563 | list($text) = explode('@', $text); 564 | $text = preg_replace('/[^a-z0-9]/i', ' ', $text); 565 | return $text; 566 | } 567 | 568 | 569 | ?> 570 | --------------------------------------------------------------------------------