<?php # WikiMedia Perforce Extension # # This extension adds support for linking and displaying Perforce # objects such as changes, jobs, and file paths. It works in tandem # with a browse mode P4Web to provide easy access to Perforce data. # In the future it may include the ability to get formatted lists # of jobs and changes. # Alert the user that this is not a valid entry point to MediaWiki # if they try to access the extension file directly. if (!defined('MEDIAWIKI')) { echo <<<EOT To install this plugin, add a section like the following to LocalSettings.php: require_once("\$IP/extensions/Perforce/Perforce.php"); \$wgP4EXEC = 'p4'; //path to the p4 client executable \$wgP4PORT = 'perforce:1666'; //Perforce server address:port \$wgP4USER = 'user'; //Perforce user with at least "list" access \$wgP4CLIENT = 'client'; //Perforce client that maps all files of interest \$wgP4PASSWD = 'password'; //Password/ticket for that user \$wgP4WEBURL = "http://computer.perforce.com:8080/"; The following setting is not needed if you have a server at 2009.1 or higher. \$wgP4DVER = 2010.1; EOT; exit( 1 ); } $dir = dirname(__FILE__) . '/'; require_once('SpecialPage.php'); $wgHooks['ParserFirstCallInit'][] = 'wfSetupPerforce'; $wgHooks['LanguageGetMagic'][] = 'wfPerforceLanguageGetMagic'; $wgHooks['OutputPageParserOutput'][] = 'wfPerforceParserOutput'; $wgAjaxExportList[] = 'wfPerforceVariantsAjax'; $wgAutoloadClasses['Perforce'] = $dir . 'Perforce.php'; $wgExtensionMessagesFiles['Perforce'] = $dir . 'Perforce.msg.php'; $wgSpecialPages['Perforce'] = 'Perforce'; $version = '$Change: 7864 $'; $version = substr( $version, 9, -2 ); $wgExtensionCredits['parserhook'][] = $wgExtensionCredits['specialpage'][] = array ( 'name' => 'Perforce', 'version' => '@'.$version.'', 'url' => 'http://public.perforce.com/wiki/Perforce_MediaWiki_extension', 'author' => array('Matt Attaway', 'Sam Stafford', 'Marc Wensauer'), 'description' => 'Display Perforce data in wiki pages', ); function wfSetupPerforce( &$parser ) { $parser->setHook( 'p4change', 'tagP4Change' ); $parser->setHook( 'p4changes', 'tagP4Changes' ); $parser->setHook( 'p4print', 'tagP4Print' ); $parser->setHook( 'p4variants', 'tagP4Variants' ); $parser->setFunctionHook( 'p4changes', 'parseP4Changes' ); $parser->setFunctionHook( 'p4chgcats', 'parseP4ChgCats' ); $parser->setFunctionHook( 'p4graph', 'parseP4Graph' ); $parser->setFunctionHook( 'p4info', 'parseP4Info' ); $parser->setFunctionHook( 'p4job', 'parseP4Job' ); $parser->setFunctionHook( 'p4jobs', 'parseP4Jobs' ); $parser->setFunctionHook( 'p4print', 'parseP4Print' ); $parser->setFunctionHook( 'p4variants', 'parseP4Variants' ); return true; } function wfPerforceLanguageGetMagic( &$magicWords, $langCode = "en" ) { switch( $langCode ) { default: # mind the ws and Ws. $magicWords['p4changes'] = array ( 0, 'p4changes' ); $magicWords['p4chgcats'] = array ( 0, 'p4chgcats' ); $magicWords['p4graph'] = array ( 0, 'p4graph' ); $magicWords['p4info'] = array ( 0, 'p4info' ); $magicWords['p4job'] = array ( 0, 'p4job' ); $magicWords['p4jobs'] = array ( 0, 'p4jobs' ); $magicWords['p4print'] = array ( 0, 'p4print' ); $magicWords['p4variants'] = array ( 0, 'p4variants' ); } return true; } function wfPerforceParserOutput( &$outputPage, $parserOutput ) { if ( !empty( $parserOutput->mPerforceJavascriptTag ) ) { global $wgJsMimeType, $wgScriptPath; $outputPage->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgScriptPath}/extensions/Perforce/Perforce.js?1\">" . "</script>\n" ); } return true; } class Perforce extends SpecialPage { function Perforce() { SpecialPage::SpecialPage("Perforce"); } function execute( $par ) { global $wgRequest, $wgOut; $this->setHeaders(); $param = $wgRequest->getText('param'); $pars = explode( '/', $par ); $fn = array_shift( $pars ); $args = explode( '@@', implode( '/', $pars ) ); switch( $fn ) { case 'changes': return $this->execChanges( $args ); case 'graph': return $this->execGraph( $args ); case 'info': return $this->execInfo( $args ); case 'job': return $this->execJob( $args ); case 'jobs': return $this->execJobs( $args ); case 'print': return $this->execPrint( $args ); case 'variants': return $this->execVariants( $args ); } return $this->execDefault(); } function execDefault() { global $wgP4WEBURL, $wgOut; $wgOut->setPagetitle( 'Perforce' ); $out = <<<EOO ==Server info== {{#p4info:}} ==Recent changes== {{#p4changes:7|//...|long}} ==Links== * [$wgP4WEBURL Browse this server via P4Web] * [http://www.perforce.com Perforce Software] * [http://www.perforce.com/perforce/technical.html Perforce Technical Documentation] * [http://kb.perforce.com Perforce Knowledge Base] * [http://public.perforce.com/ Perforce Public Depot] * [http://public.perforce.com/wiki/Perforce_MediaWiki_extension About this extension] EOO; $wgOut->addWikiText( $out ); } function execChanges( $args ) { global $wgOut, $wgParser; if ( $args[1] && $args[1] != '//...' ) $toPath = ' to '.$args[1]; if ( $args[3] ) $byUser = ' by user '.$args[3]; if ( $toPath || $byUser ) $made = ' made'; if ( !$args[0] ) $args[0] = '40'; $wgOut->setPagetitle( 'Last '.$args[0].' changes'.$made.$toPath.$byUser ); $wgOut->addWikiText( parseP4Changes( $wgParser, $args[0], $args[1], $args[2], $args[3] ) ); } function execGraph( $args ) { global $wgOut, $wgParser; $wgOut->setPagetitle( 'Graph of '.$args[0] ); $wgOut->addWikiText( parseP4Graph( $wgParser, $args[0], $args[1], $args[2] ) ); } function execInfo( $args ) { global $wgOut, $wgParser; $wgOut->setPagetitle( 'Perforce server info' ); $wgOut->addWikiText( parseP4Info( $wgParser ) ); } function execJob( $args ) { global $wgOut, $wgParser; $wgOut->setPagetitle( 'Perforce job '.$args[0].' '.$args[1] ); array_unshift( $args, $wgParser ); $out = call_user_func_array( 'parseP4Job', $args ); $wgOut->addWikiText( $out[0] ); } function execJobs( $args ) { global $wgOut, $wgParser; $args = str_replace( '_', ' ', $args ); $wgOut->setPagetitle( 'Perforce jobs: '.$args[0] ); if ( !$args[0] ) $wgOut->setPagetitle( 'Perforce jobs' ); array_unshift( $args, $wgParser ); $out = call_user_func_array( 'parseP4Jobs', $args ); $wgOut->addWikiText( $out[0] ); } function execPrint( $args ) { global $wgOut, $wgParser; $wgOut->setPagetitle( $args[0] ); $mode = $args[1]; if ( !$mode ) $mode = 'raw'; $wgOut->addWikiText( parseP4Print( $wgParser, $args[0], $mode ) ); } function execVariants( $args ) { global $wgOut; $wgOut->setPagetitle( 'Variants of '.$args[0] ); $wgOut->addHtml( getP4Variants( $args[0] ) ); } } # {{#p4changes:num|path|brief/long/full|user}} function parseP4Changes( &$parser, $num = '', $path = '', $desc = '', $user = '' ) { if ( !$path ) $path = '//...'; $tag = '<p4changes'; if ( $num != '' ) $tag .= " num=\"$num\""; if ( $path != '' ) $tag .= " path=\"$path\""; if ( $desc != '' ) $tag .= " desc=\"$desc\""; if ( $user != '' ) $tag .= " user=\"$user\""; $tag .= '/>'; return array($tag, 'noparse' => false); } # {{#p4chgcats:path|fmt|keyword1|cat1|keyword2|cat2|...}} function parseP4ChgCats( &$parser, $path = '', $pre = '*', $fmt = '' ) { $parser->disableCache(); $cats = array(); // list of cats $maps = array(); // key->cat mappings // Build array of categories and keyword map. $arg = 4; //number of named args while ( func_num_args() > $arg + 1 ) { $key = func_get_arg( $arg ); $arg++; $cat = func_get_arg( $arg ); $arg++; $maps[ $key ] = $cat; if ( !in_array( $cat, $cats ) ) { $cats[] = $cat; $chgs[$cat] = array(); } } // Get ze changes! $cmdline = buildP4Cmd(); $cmdline .= ' -Ztag -Zspecstring changes -l -m 10000 '.wfEscapeShellArg( $path ); $output = array(); // mock up specdef for getSpecFields() $output[] = '... specdef change; ;;time; ;;user; ;;client; ;;status; ;;desc; ;;'; exec( $cmdline, $output ); $data = getSpecFields( $output ); array_shift( $data ); // shift off field list $result = ''; foreach( $cats as $cat ) { $result .= $cat."\n"; foreach( $data as $chg ) { $show = false; foreach( $maps as $key => $map ) { if ( $map != $cat ) continue; if ( strpos( $chg['desc'], $key ) === false ) continue; $show = true; } if ( !$show ) continue; $desc = $chg['desc']; formatField( $fmt, $desc ); $result .= $pre.'<p4change>'.$chg['change'].'</p4change>: '; $result .= $desc."\n"; } } return array($result, 'noparse' => false); } function tagP4Changes( $input, $argv, $parser ) { return renderRecentChanges( $input, $argv, $parser, 'html' ); } # {{#p4graph:path|constraint|server|dot}} function parseP4Graph( &$parser, $path, $const= '', $server = '', $dot = '' ) { global $wgP4DVER; $flag = '-1 '; if ( $wgP4DVER && $wgP4DVER < 2009.1 ) $flag = ''; $rankdir = 'LR'; if ( $const == 'file' ) $rankdir = 'TD'; $cmdline = buildP4Cmd( $server ); $cmdline .= ' filelog ' . $flag . wfEscapeShellArg( $path ) . ' 2>&1'; $filelog = array(); exec( $cmdline, $filelog ); $out = "<graphviz>\n"; $out .= ' digraph G { rankdir='.$rankdir.'; node [shape=box];'; $out .= "\n"; $todo = array(); $done = array(); $changes = array(); getP4Graph( $filelog, $out, $todo, $done, $changes, $const ); while ( count( $todo ) ) { $file = array_pop( $todo ); if ( !$file ) continue; $filelog = array(); $cmdline = buildP4Cmd( $server ); $cmdline .= ' filelog ' . $flag . wfEscapeShellArg( $file ); exec( $cmdline, $filelog ); getP4Graph( $filelog, $out, $todo, $done, $changes, $const ); } if ( $const == 'change' && count( $changes ) > 1 ) { array_unique( $changes ); sort( $changes, SORT_NUMERIC ); $fchange = array_shift( $changes ); $out .= ' "'.$fchange.'" [style=invis];'."\n"; $out .= ' "'.$fchange; $lchange = array_pop( $changes ); foreach( $changes as $change ) { $out .= '" -> "'.$change.'" [style=invis weight=100];'."\n"; $out .= ' "'.$change.'" [style=invis];'."\n"; $out .= ' "'.$change; } $out .= '" -> "'.$lchange.'" [style=invis weight=100];'."\n"; $out .= ' "'.$lchange.'" [style=invis];'."\n"; } if ( $const == 'file' && count( $done ) > 1 ) { $files = $done; array_unique( $files ); sort ( $files ); $ffile = array_shift( $files ); $out .= ' "'.$ffile.'" [style=invis];'."\n"; $out .= ' "'.$ffile; $lfile = array_pop( $files ); foreach( $files as $file ) { $out .= '" -> "'.$file.'" [style=invis weight=100];'."\n"; $out .= ' "'.$file.'" [style=invis];'."\n"; $out .= ' "'.$file; } $out .= '" -> "'.$lfile.'" [style=invis weight=100];'."\n"; $out .= ' "'.$lfile.'" [style=invis];'."\n"; } if ( $dot ) $out .= ' '.$dot."\n"; $out .= " }\n"; $out .= "</graphviz>"; if ( !count($done) ) return $out; #error #Trim paths if possible. $firstFile = array_shift( $done ); array_unshift( $done, $firstFile ); $firstCmn = $firstFile; $lastCmn = $firstFile; foreach( $done as $file ) { $firstCmn = strFirstCommon( $firstCmn, $file ); $lastCmn = strLastCommon( $lastCmn, $file ); } #If there's only one file, just use the filename. if ( $firstCmn == $firstFile ) { $firstCmn = substr($firstFile,0,-1*(strlen(strrchr($firstFile,'/'))-1)); $lastCmn = ''; } $firstLen = strlen( $firstCmn ); $lastLen = strlen( $lastCmn ); foreach( $done as $file ) { $newname = substr( $file, $firstLen ); if ( $lastLen ) $newname = substr( $newname, 0, -1 * $lastLen ); $out = str_replace( $file, $newname, $out ); } return array($out, 'noparse' => false); } # {{#p4info:}} function parseP4Info( &$parser ) { $out = ''; $cmdline = buildP4Cmd() . ' info'; $info = array(); exec( $cmdline, $info ); $out .= "{| id=p4info\n"; foreach( $info as $line ) { $regs = array(); if ( !preg_match( '/^(Server [^:]+:)(.*)/', $line, $regs ) ) continue; $out .= "|-\n"; $out .= "| '''$regs[1]'''\n"; $out .= "| \n"; $out .= "| $regs[2]\n"; } $out .= "|}"; return $out; } # {{#p4job:job|field|format}} function parseP4Job( &$parser, $job = '', $field = '', $format = '' ) { if ( $job == '' ) { return array( '<b>Please specify a job.</b>', 'noparse' => false ); } global $wgP4WEBURL; if ( $field == '' ) { return array( '['.$wgP4WEBURL.$job.'?ac=111 '.$job.']', 'noparse' => false ); } $parser->disableCache(); $cmdline = buildP4Cmd(); $cmdline .= ' -Ztag -Zspecstring job -o ' . wfEscapeShellArg( $job ); $output = array(); exec( $cmdline, $output ); $fields = getSpecFields( $output ); $result = isset($fields[$job][strtolower($field)]) ? $fields[$job][strtolower($field)] : ''; if ( $format ) formatField( $format, $result ); return array( $result, 'noparse' => false ); } # {{#p4jobs:expr|fields|maxjobs|format| # tableattr|trattr|tdattr|query1|action1|...}} function parseP4Jobs( &$parser, $expr='', $fields='', $maxjobs='', $format='', $tableattr='', $trattr='', $tdattr='' ) { $parser->disableCache(); if ( !$maxjobs ) $maxjobs = '20'; $mjobs = intval($maxjobs); if ( $mjobs > 100 || $mjobs < 1 ) $mjobs = 100; $cmdline = buildP4Cmd(); $cmdline .= ' -Ztag -Zspecstring jobs -r -m'.$mjobs; $expr = htmlspecialchars_decode( $expr ); // angle brackets! if ( $expr ) $cmdline .= ' -e '.wfEscapeShellArg( $expr ); $output = array(); exec( $cmdline, $output ); $data = getSpecFields( $output ); $rules = array(); $arg = 8; //number of standard args while ( func_num_args() > $arg + 1 ) { $rule = array(); $rule['query'] = func_get_arg( $arg ); $arg++; $rule['action'] = func_get_arg( $arg ); $arg++; $rule['jobs'] = array(); applyJobQuery( $rule, $data, $expr, $mjobs ); $rules[] = $rule; } if ( $fields ) { $fields = preg_split( '/\s+/', $fields ); } else { $fields = array_slice( $data['@'], 0, 5 ); } array_shift( $data ); # shift off the specdef data sort( $data ); $fields = array_reverse( $fields ); foreach ( $fields as &$fref ) { $o = +1; if ( substr( $fref, 0, 1 ) == '!' ) { $o = -1; $fref = substr( $fref, 1 ); } # Insert current ordering information into array elements. Ech. foreach( $data as $k => &$jref ) { $jref['@'] = $k; } usort( $data, create_function ( '$a,$b', 'return compKey($a,$b,"'.addslashes(strtolower($fref)).'",'.$o.');' ) ); } $fields = array_reverse( $fields ); $index = array(); foreach( $data as $k => &$row ) { $index[$row['job']] = $k; $row['@attrib'] = ''; $row['@format'] = ''; foreach( $rules as &$rule ) { if ( $rule['query'] == 'ALL' ) $rule['jobs'][] = $row['job']; if ( $rule['query'] == 'ODD' && $k % 2 == 1 ) $rule['jobs'][] = $row['job']; if ( $rule['query'] == 'EVEN' && $k % 2 == 0 ) $rule['jobs'][] = $row['job']; } } applyJobRules( $rules, $data, $index ); $result = '{|'.$tableattr."\n"; $result .= '|-'.$trattr."\n"; foreach( $fields as $f ) { $result .= '!'.$f."\n"; } $result .= "\n"; foreach( $data as $job ) { $result .= '|-'.$trattr.$job['@attrib']."\n"; foreach( $fields as $f ) { $value = $job[strtolower($f)]; $result .= '|'.$tdattr.'|'; if ( $f == 'Job' ) $value = '{{#p4job:'.$value.'}}'; formatField( $format.$job['@format'], $value, $f ); $result .= $value; $result .= "\n"; } $result .= "\n"; } $result .= '|}'; return array( $result, 'noparse' => false ); } # {{#p4print:path|raw/text/wiki}} function parseP4Print( &$parser, $path = '', $mode = 'raw' ) { if ( $mode == 'raw' ) { return array("<p4print path=\"$path\"/>", 'noparse' => false); } $cmdline = buildP4Cmd(); $cmdline .= ' print -q ' . wfEscapeShellArg( $path ); $cmdline .= ' 2>&1'; $text = ""; $output = array(); exec( $cmdline, $output ); foreach( $output as $line ) { if ( $mode == 'text' ) { $line = ' ' . $line; } $text = $text . $line . "\n" ; } return $text; } function tagP4Print( $input, $argv, $parser ) { $text = '<pre>'; $text .= htmlspecialchars( parseP4Print( $parser, $argv['path'], 'wiki' ) ); $text .= '</pre>'; return $text; } # {{#p4variants:path}} function parseP4Variants( &$parser, $path = '' ) { return array('<p4variants path="'.trim($path).'"/>', 'noparse' => false); } function tagP4Variants( $input, $argv, $parser ) { global $wgUseAjax, $wgScriptPath; if ( !$wgUseAjax ) return "This MediaWiki instance does not support Ajax."; $parser->mOutput->mPerforceJavascriptTag = true; # flag for use by wfPerforceParserOutput $path = $argv["path"]; if( $path == '' ) return "Please provide a depot path (path attribute)."; $nocache = time(); $html = ''; $html .= '<div class="p4VariantSection">'; $html .= '<div class="p4VariantHead">'; $html .= renderPerforcePathLink( $path, TRUE ); $html .= ' <span class="p4VariantButton">['; $html .= '<a href="#" onclick="'; $html .= "this.href='javascript:void(0)'; perforceGetVariants( '$path', this, '$nocache' )"; $html .= '" title="Search for branches and find outstanding changes -- may take a while!">find variants</a>'; $html .= ']</span>'; $html .= '</div>'; $html .= '<div class="p4VariantBody" style="display:none"><ul><img class="p4VariantLoading" alt="loading..." src="'; $html .= "$wgScriptPath/extensions/Perforce/P4_Busy.gif"; $html .= '"/></ul>'; $html .= '</div>'; $html .= '</div>'; return $html; } function wfPerforceVariantsAjax( $path, $nocache ) { #nocache is a random number passed around to keep this from getting cached. $response = new AjaxResponse(); $response->addText( getP4Variants( $path ) ); return getP4Variants( $path ); return $response; } function tagP4Change( $input, $argv, $parser ) { global $wgP4EXEC; global $wgP4PORT; global $wgP4USER; global $wgP4PASSWD; global $wgP4WEBURL; if( isset($argv["style"]) ) { $range = '@' . $input . ',' . $input; $cmdline = wfEscapeShellArg( $wgP4EXEC ) . " -u " . wfEscapeShellArg( $wgP4USER ) . " -p " . wfEscapeShellArg( $wgP4PORT ); if( $wgP4PASSWD != "" ) $cmdline .= " -P " . wfEscapeShellArg( $wgP4PASSWD ); $cmdline .= " changes " . wfEscapeShellArg( $range ); $text = `{$cmdline}`; return formatShortChange( $text ); } else return "<a href=\"$wgP4WEBURL" . $input . "?ac=10\">" . htmlspecialchars( $input ) . "</a>"; } function renderRecentchanges( $input, $argv, &$parser, $format ) { $parser->disableCache(); $path = isset($argv["path"]) ? $argv["path"] : ""; $num = isset($argv["num"]) ? $argv["num"] : ""; $desc = isset($argv["desc"])? $argv["desc"] : ""; $user = isset($argv["user"])? $argv["user"] : ""; if( $path == "" || $num == "" ) return "Please provide both a depot path(path attribute) and the number of changes to show(num attribute)."; $cmdline = buildP4Cmd(); $cmdline .= " changes -s submitted -m" . escapeshellarg( $num ) . " "; if ( $desc == "long" ) $cmdline .= "-L "; if ( $desc == "full" ) $cmdline .= "-l "; if ( $user != "" ) $cmdline .= "-u " . escapeshellarg( $user ) . " "; if ( $path != '//...' ) $cmdline .= escapeshellarg( $path ) . " "; $cmdline .= '2>&1'; $changes = array(); exec( $cmdline, $changes ); $isDarkStripe = true; $output = "<ul class=\"zebra\">"; $inList = false; $inListList = false; foreach( $changes as $value ) { if ( !preg_match( '/^Change [0-9]+ on/', $value ) ) { if ( !$inListList ) { $output .= '<ul>'; $inListList = true; } $output .= htmlspecialchars( $value ); if ( $value ) $output .= '<br/>'; continue; } if ( $inListList ) { $output .= '</ul>'; $inListList = false; } if ( $inList ) { $output .= "</li>\n"; $inList = false; } if( $isDarkStripe ) $output .= "<li class=\"zebra\">"; else $output .= "<li>"; $inList = true; $output .= formatShortChange( $value, $format ); $isDarkStripe = !$isDarkStripe; } if ( $inListList ) { $output .= '</ul>'; $inListList = false; } if ( $inList ) { $output .= "</li>\n"; $inList = false; } $output .= "</ul>"; return $output; } function formatShortChange( $text, $format='html', $newwin=FALSE ) { global $wgP4WEBURL; $target = ''; if ( $newwin ) $target = 'target="_blank" '; $pattern[0] = '/Change (\d+) /'; $pattern[1] = '/ (\S+)@(\S+)/'; switch( $format ) { default: case 'html': $replacement[0] = 'Change <a '.$target.'href="' . $wgP4WEBURL . '${1}?ac=10\">${1}</a> '; $replacement[1] = ' <a '.$target.'href="' . $wgP4WEBURL . '${1}?ac=17">${1}</a>@<a '.$target.'href="' . $wgP4WEBURL . '${2}?ac=15">${2}</a> '; break; case 'wiki': $replacement[0] = 'Change [' . $wgP4WEBURL . '${1}?ac=10 ${1}] '; $replacement[1] = ' [' . $wgP4WEBURL . '${1}?ac=17 ${1}]@[' . $wgP4WEBURL . '${2}?ac=15 ${2}] '; break; } return preg_replace( $pattern, $replacement, $text ); } function renderPerforcePathLink( $path, $newwin=FALSE ) { global $wgP4WEBURL; $target = ''; $ac = 22; if ( ( strpos( $path, '...' ) !== FALSE ) || ( strpos( $path, '*' ) !== FALSE ) ) $ac = 69; if ( $newwin ) $target = 'target="_blank" '; return '<a '.$target.'href="'. $wgP4WEBURL . $path . '?ac='.$ac.'">'.htmlspecialchars( $path ).'</a>'; } function getP4Variants( $path ) { global $wgP4EXEC; global $wgP4PORT; global $wgP4USER; global $wgP4CLIENT; global $wgP4PASSWD; $path = trim( $path ); if ( substr( $path, -1 ) == '/' ) { $path .= '...'; } $html = '<ul>'; $p4Cmd = $wgP4EXEC . " -u " . $wgP4USER . " -p " . $wgP4PORT . " -c " . $wgP4CLIENT; if( $wgP4PASSWD != "" ) $p4Cmd .= " -P " . $wgP4PASSWD; #Infer branch relationships from integed output. $toFile = ''; $fromFile = ''; $pleft = $path; $pwild = ''; $pright = ''; if ( substr( $path, -1 ) == '*' ) { $pleft = substr( $path, 0, -1 ); $pwild = substr( $path, -1 ); } if ( substr( $path, -3 ) == '...' ) { $pleft = substr( $path, 0, -3 ); $pwild = substr( $path, -3 ); } $pllen = strlen( $pleft ); $prlen = strlen( $pright ); if ( ( strpos( $pleft, '...' ) !== FALSE ) || ( strpos( $pleft, '*' ) !== FALSE ) ) return '<b>Embedded wildcard in '.htmlspecialchars( $path ).' -- unable to figure out branch relationships.</b>'; $cmdline = $p4Cmd . ' -Ztag integrated ' . escapeshellarg( $path ); $descriptors = array ( 1 => array("pipe", "w") ); $branches = array(); $integedproc = proc_open( $cmdline, $descriptors, $pipes ); if ( !is_resource($integedproc) ) return 'Unable to get branch information.'; while ( !feof($pipes[1]) ) { $line = substr( fgets( $pipes[1] ), 0, -1 ); #chomp newline if ( substr( $line, 0, 11 ) == '... toFile ' ) $toFile = substr( $line, 11 ); if ( substr( $line, 0, 13 ) == '... fromFile ' ) $fromFile = substr( $line, 13 ); if ( $toFile && $fromFile ) { #toFile is our path, even if it's really the "from" of the integ. Handy! if ( substr( $toFile, 0, $pllen ) == $pleft ) #this should always be true { $pright = substr( $toFile, $pllen ); $prlen = strlen( $pright ); if ( !$prlen || substr( $fromFile, -$prlen ) == $pright ) #check for a path match { if ( $prlen ) $branch = substr( $fromFile, 0, -$prlen ) . $pwild; else $branch = $fromFile; $branches[$branch] = $branch; } } $toFile = ''; $fromFile = ''; } } sort ( $branches ); if ( !count( $branches ) ) { return '<ul><i>No branches found.</i></ul>'; } set_time_limit( 300 ); #We now have a list of $branches that each corresponds to $path. foreach( $branches as $branch ) { $cmdline = $p4Cmd . ' interchanges ' . escapeshellarg( $branch ) . ' ' . escapeshellarg( $path ); $changes = array(); exec( $cmdline, $changes ); $bvar = FALSE; foreach( $changes as $change ) { $cnum = ''; $cnums = array(); if ( preg_match( '/^Change (\d+) /', $change, $cnums ) ) $cnum = $cnums[1]; else continue; $cvar = FALSE; $cmdline = $p4Cmd . ' -Ztag integrate -n ' . escapeshellarg( $branch.'@'.$cnum.',@'.$cnum ) . ' ' . escapeshellarg( $path ); $integs = array(); exec( $cmdline, $integs ); foreach( $integs as $integ ) { if ( $integ == '... action integrate' ) { $cvar = TRUE; break; } } if ( $cvar ) { if ( !$bvar ) { $bvar = TRUE; $html .= '<li>'; $html .= renderPerforcePathLink( $branch, TRUE ); $html .= '<ul>'; } $html .= '<li>'; $html .= formatShortChange( $change, 'html', TRUE ); $html .= '</li>'; } } if ( $bvar ) { $html .= '</ul></li>'; } } if ( strpos( $html, '<li>' ) === FALSE ) { $html .= '<li><i>No branches have outstanding changes.</i></li>'; } $html .= '</ul>'; return $html; } # Takes array of filelog output and running count # of what files need doing and what files are done. # Returns GraphViz output. function getP4Graph( &$filelog, &$out, &$todo, &$done, &$changes, &$const ) { $file = ''; $rev = ''; $error = ''; $dirty = FALSE; foreach( $filelog as $line ) { $regs = array(); if ( preg_match( '/^... ... (.*) (\/\/[^#]*)#(.*)/', $line, $regs ) ) { if ( !in_array($regs[2],$todo) && !in_array($regs[2],$done) ) { array_push ( $todo, $regs[2] ); } if ( strpos( $regs[1], ' by' ) || strpos( $regs[1], ' into' ) ) continue; $lbl = $regs[1]; $clr = 'blue'; $sty = 'solid'; switch ( $lbl ) { case 'branch from': case 'copy from': case 'delete from': $lbl = substr( $lbl, 0, -5 ); break; case 'moved from': $lbl = substr( $lbl, 0, -5 ); $clr = 'red'; break; case 'merge from': $lbl = substr( $lbl, 0, -5 ); $sty = 'dashed'; break; case 'ignored': $lbl = substr( $lbl, 0, -1 ); $sty = 'dotted'; break; case 'edit from': $lbl = substr( $lbl, 0, -5 ); $sty = 'dashed'; $clr = 'red'; break; } if ( $dirty ) $clr = 'red'; $src = $regs[3]; if ( strpos( $src, '#' ) ) $src = substr( $src, strpos( $src, '#' ) + 1 ); if ( $file && $rev ) { $out .= ' "'.$regs[2].'#'.$src.'" -> "'.$file.'#'.$rev.'"'; $out .= ' [label="'.$lbl.'" color="'.$clr.'" fontcolor="'.$clr.'" style="'.$sty.'" weight=1];'; $out .= "\n"; } } else if ( preg_match( '/^... #([0-9]+) change ([0-9]+) ([a-z\/]+) /', $line, $regs ) ) { if ( $file && $rev ) { $out .= ' "'.$file.'#'.$regs[1].'" -> "'.$file.'#'.$rev.'" [weight=1000];'."\n"; } $rev = $regs[1]; $dirty = ( $regs[3] == 'edit' || $regs[3] == 'add' ); if ( $const == 'change' ) { array_push( $changes, $regs[2] ); $out .= ' { rank=same "'.$file.'#'.$regs[1].'" -> "'.$regs[2].'" [style=invis]; }'."\n"; } if ( $const == 'file' ) { $out .= ' { rank=same "'.$file.'#'.$regs[1].'" -> "'.$file.'" [style=invis]; }'."\n"; } } else if ( preg_match( '/^\/\//', $line, $regs ) ) { $file = $line; $rev = ''; array_push( $done, $file ); if ( in_array( $file, $todo ) ) { unset( $todo[array_search($file,$todo)] ); } } else $error .= ' ' .$line . "\n"; } if ( $error ) { $out = ' <font color="red">' . substr( $error, 1 ) . "</font>\n" . $out; } return $out; } function buildP4Cmd( $server='' ) { global $wgP4EXEC; global $wgP4PORT; global $wgP4USER; global $wgP4PASSWD; global $wgP4ALTPORTS; $cmdline = $wgP4EXEC . " -u " . $wgP4USER ; if( $wgP4PASSWD != "" ) $cmdline .= " -P " . $wgP4PASSWD ; if ( $server == '' || $server == $wgP4PORT ) { $cmdline .= " -p " . $wgP4PORT; } else { if ( is_array( $wgP4ALTPORTS) && in_array( $server, $wgP4ALTPORTS ) ) $cmdline .= " -p " . $server; else $cmdline .= " -p " . $wgP4PORT; } return $cmdline; } #returns the common leading substring of two strings, split on '/'. function strFirstCommon( $str1, $str2 ) { $path1 = explode( '/', $str1 ); $path2 = explode( '/', $str2 ); $cmn = array(); while ( count( $path1 ) > 1 && count( $path2 ) > 1 ) { $s1 = array_shift( $path1 ); $s2 = array_shift( $path2 ); if ( $s1 != $s2 ) break; array_push( $cmn, $s1 ); } if ( !count( $cmn ) ) return ''; return implode( '/', $cmn ) . '/'; } #returns the common trailing substring of two strings, split on '/'. function strLastCommon( $str1, $str2 ) { $path1 = explode( '/', $str1 ); $path2 = explode( '/', $str2 ); $cmn = array(); while ( count( $path1 ) > 1 && count( $path2 ) > 1 ) { $s1 = array_pop( $path1 ); $s2 = array_pop( $path2 ); if ( $s1 != $s2 ) break; array_unshift( $cmn, $s1 ); } if ( !count( $cmn ) ) return ''; return '/' . implode( '/', $cmn ); } #takes an array of "p4 -Ztag -Zspecstring specs..." output, #returns an array of arrays of spec fields. function getSpecFields( $output ) { $fields = array(); $fields['@'] = array(); $specstring = array_shift( $output ); $specstring = substr( $specstring, 12 ); # remove '... specdef ' $fields['@'] = preg_split( '/;;/', $specstring ); foreach( $fields['@'] as &$field ) { $field = array_shift( preg_split( '/;/', $field ) ); } $id = $fields['@'][0]; # name of spec type, e.g. 'job' $i = -1; $spec = array(); foreach( $output as $line ) { $line .= "\n"; $m = array(); if ( preg_match( '/^\.\.\. ([^\s]+)/', $line, $m ) ) { if ( $m[1] == $id ) { # Start of a new spec. if ( array_key_exists( strtolower($id), $spec ) ) { foreach( $spec as &$f ) { $f = chop( $f ); } $fields[$spec[strtolower($id)]] = $spec; } $i = -1; $spec = array(); } if ( in_array( $m[1], $fields['@'] ) && array_search( $m[1], $fields['@'] ) > $i ) { # Start of a new field. $line = substr( $line, 5 + strlen($m[1]) ); #remove '... Field ' $i = array_search( $m[1], $fields['@'] ); $spec[strtolower($fields['@'][$i])] = ''; } } if ( $i < 0 ) { continue; } $spec[strtolower($fields['@'][$i])] .= $line; } if ( array_key_exists( strtolower($id), $spec ) ) { foreach( $spec as &$f ) { $f = chop( $f ); } $fields[$spec[strtolower($id)]] = $spec; } return $fields; } # Compare two arrays by value associated with a particular key. function compKey( $arr1, $arr2, $key, $order = 1 ) { # Sort arrays that don't have the key to the front for easy removal. if ( !array_key_exists( $key, $arr1 ) && !array_key_exists( $key, $arr2 ) ) return 0; if ( !array_key_exists( $key, $arr1 ) ) return -1; if ( !array_key_exists( $key, $arr2 ) ) return +1; if ( $arr1[$key] < $arr2[$key] ) return -1 * $order; if ( $arr1[$key] > $arr2[$key] ) return +1 * $order; # Existing order is kept in ['@']; preserve if there. if ( array_key_exists( '@', $arr1 ) && array_key_exists( '@', $arr2 ) ) { if ( $arr1['@'] < $arr2['@'] ) return -1; if ( $arr1['@'] > $arr2['@'] ) return +1; } return 0; } # Takes a formatting string and field to format (in-place). function formatField( $format, &$field, $fname = '' ) { $field = str_replace( '!', '!', str_replace( '|', '|', $field ) ); $fname = strtolower( $fname ); $fmt = preg_split( '/\s+/', $format ); foreach( $fmt as $f ) { $hpos = strrpos( $f, '#' ); if ( $hpos ) { $flimit = strtolower( substr( $f, $hpos + 1 ) ); if ( $flimit && $flimit != $fname ) continue; $f = substr( $f, 0, $hpos ); } if ( !strncasecmp( $f, 'template:', 9 ) ) { $f = substr( $f, 9 ); $field = '{{'.$f.'|'.$field.'}}'; continue; } if ( strpos( $f, 'chars' ) ) { $field = substr( $field, 0, intval( substr( $f, 0, -5 ) ) ); } if ( strpos( $f, 'words' ) ) { $words = explode( ' ', $field ); $words = array_slice( $words, 0, intval( substr( $f, 0, -5 ) ) ); $field = implode( ' ', $words ); } if ( strpos( $f, 'lines' ) ) { $lines = explode( "\n", $field ); $lines = array_slice( $lines, 0, intval( substr( $f, 0, -5 ) ) ); $field = implode( "\n", $lines ); } if ( strpos( $f, 'paras' ) ) { $paras = explode( "\n\n", $field ); $paras = array_slice( $paras, 0, intval( substr( $f, 0, -5 ) ) ); $field = implode( "\n\n", $paras ); } if ( strpos( $f, 'sents' ) ) { $sents = preg_split( '/(\s+[^\s]+\s+[^\s]+[\.!?]+\s+)/', $field, 0, PREG_SPLIT_DELIM_CAPTURE ); $sents = array_slice( $sents, 0, 2 * intval( substr( $f, 0, -5 ) ) ); $field = implode( '', $sents ); } if ( $f == 'line' ) { $field = trim( preg_replace( '/\s+/', ' ', $field ) ); } if ( $f == 'raw' && strpos( $field, "\n" ) ) { $field = '<pre><nowiki>'.$field.'</nowiki></pre>'; } if ( $f == 'text' && strpos( $field, "\n" ) ) { $lines = explode( "\n", $field ); $field = "\n ".implode( "\n ", $lines ); } } } # Annotate an rule array with list of matching jobs. function applyJobQuery( &$rule, $table, $basequery, $maxjobs ) { $query = $rule['query']; $query = trim( $query ); if ( $query == 'ALL' || $query == 'ODD' || $query == 'EVEN' ) return; $query = strtolower( $query ); $field = ''; $value = ''; if ( !strpos( $query, ' ' ) && !strpos( $query, '&' ) && !strpos( $query, '|' ) && !strpos( $query, '^' ) && !strpos( $query, '*' ) ) { // We can handle simple queries by ourselves. I hope. $pair = explode( '=', $query ); if ( count( $pair ) == 2 ) { $field = $pair[0]; $value = $pair[1]; } else if ( count( $pair ) == 1 ) { $value = $pair[0]; } } if ( $value ) { // Handle it. foreach ( $table as $row ) { $found = false; if ( $field ) { if ( stripos( $row[$field], $value ) !== false ) $found = true; } else { foreach ( $row as $k => $fv ) { if ( stripos( $fv, $value ) !== false ) { $found = true; } } } if ( $found ) $rule['jobs'][] = $row['job']; } } else { // Couldn't handle it. Run a new job query. if ( $basequery ) $newquery = '('.$basequery.') ('.$query.')'; else $newquery = $query; $cmdline = buildP4Cmd(); $cmdline .= ' jobs -m '.$maxjobs.' -e '.escapeshellarg( $newquery ); $jobs = array(); exec( $cmdline, $jobs ); foreach( $jobs as $j ) { $jf = explode( ' ', $j ); if ( array_key_exists( $jf[0], $table ) ) $rule['jobs'][] = $jf[0]; } } } # Process per-row job "rules" and annotate job table with results. function applyJobRules( $rules, &$table, $index ) { foreach( $rules as $rule ) { $key = ''; if ( !strcasecmp( substr( $rule['action'], 0, 7 ), 'attrib:' ) ) { $key = '@attrib'; $action = substr( $rule['action'], 7 ); } if ( !strcasecmp( substr( $rule['action'], 0, 7 ), 'format:' ) ) { $key = '@format'; $action = substr( $rule['action'], 7 ); } if ( !$key ) continue; foreach( $rule['jobs'] as $j ) { $table[$index[$j]][$key] .= ' '.$action; } } } ?>
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#4 | 7864 | Marc Wensauer |
Update tag and parser function registration process to follow current (as of MediaWiki 1.16.1) conventions. Modify return of parser functions to allow the output to be fully parsed; this allows parser functions that output tags to have the resulting tags rendered. |
||
#3 | 7863 | Marc Wensauer |
Fixing to run under PHP 5.3: * ereg() -> preg_match() * split() -> preg_split() * guarding array key lookups of non-existent keys with isset() Still need to figure out why these parser functions that simply return tags don't work: * p4changes * p4chgcats (each change listed) * p4graph * p4print (raw mode; text and wiki modes work fine) * p4variants Removed the following (deprecated?) parser extension tags: * changelist * recentchanges |
||
#2 | 7764 | Marc Wensauer |
Fix wfPerforceParserOutput() so that it works with MediaWiki 1.16.x. Minor fix to spacing. |
||
#1 | 7763 | Marc Wensauer | Branching Perforce extension for customization/experimentation. | ||
//guest/sam_stafford/mediawiki/extensions/Perforce/Perforce.php | |||||
#32 | 7758 | Sam Stafford |
New #p4chgcats parser function to produce lists of changelists according to keyword. Also extended the formatting functionality to split based on paragraph (blank line) and sentence (punctuation). |
||
#31 | 7668 | Sam Stafford |
Add "-1" flag to filelog for #p4graph on 2009.1+ servers so we don't get redundant edges on moved files (ugh). Added new $wgP4DVER which you can use to indicate your server version if you aren't on something recent; by default we assume 2009.1+. |
||
#30 | 7667 | Sam Stafford | Fix whitespace. | ||
#29 | 7666 | Sam Stafford | Update #p4graph function to properly handle "p4 move" history. | ||
#28 | 7424 | Sam Stafford |
Add Nlines format option so that (for example) the first line of a job can be snagged. |
||
#27 | 7300 | Sam Stafford | Make sure that mangled sorting parameters don't generate PHP errors. | ||
#26 | 7298 | Sam Stafford |
Two minor bug fixes: 1) Greater than/less than signs in job expressions were getting HTML-escaped. They're now unescaped so they work properly. 2) Pending changes were being included in changes queries, which is not usually desirable. The query now includes "-s submitted" implicitly. |
||
#25 | 7266 | Sam Stafford | Add hooks to invoke job functions from Special:Perforce page. | ||
#24 | 7265 | Sam Stafford |
Fix bug with specdef data getting into the table during sorting. Solved by popping it out of the table before that happens. |
||
#23 | 7264 | Sam Stafford |
#p4jobs: Add option to apply attributes or formatting to rows based on job queries. Combined with all the stuff you can already do in the formatting rules, this lets you do just about anything to the table while it's being generated. |
||
#22 | 7255 | Sam Stafford |
Make field references case-insensitive, matching the behavior of job queries. |
||
#21 | 7247 | Sam Stafford |
Add ability to limit formatting commands to specific fields, and a formatting command to apply an arbitrary template to each field value. |
||
#20 | 7243 | Sam Stafford |
Cap #p4jobs hard at 100 jobs (really large lists start hitting limits in PHP), and change default to 20. |
||
#19 | 7242 | Sam Stafford |
Add formatting capabilities to #p4jobs. Rather than 2 parameters as I'd originally envisioned, formatting for a given field is now a single string of formatting keywords, e.g.: 20words line to limit the output to 20 words and convert linebreaks to spaces so it all goes on one line. This change has also been applied to #p4job, replacing the "chars" and "normalize" parameters with a single "format" parameter that can perform both functions (and a few more). |
||
#18 | 7241 | Sam Stafford |
Work in progress on #p4jobs function. All the important bits are there, but formatting needs work. |
||
#17 | 7240 | Sam Stafford |
Add #p4job: parser function. Can either generate a P4Web link to a job or extract a particular field from the job and return its value inline, with optional parameters to limit the length of the returned value and/or normalize whitespace so it fits on one line. |
||
#16 | 6449 | Sam Stafford |
Add another esoteric undoc parameter to the #p4graph function. Whatever you pass in here gets inserted into the digraph block as-is, so that you can add extra nodes, labels, et cetera in DOT markup. Usage: {{#p4graph:path|constraint|p4port|dotmarkup}} |
||
#15 | 6339 | Sam Stafford | Add credits entry to Special Pages section. | ||
#14 | 6337 | Sam Stafford |
Use proc_open() instead of exec() to read 'p4 integrated'. I THINK that this will reduce the memory footprint by allowing each line of output to be discarded as it's processed, which might allow this function to handle larger projects without hitting PHP's memory limits. |
||
#13 | 6334 | Sam Stafford |
Add more subpages to Special:Perforce, one per parser function. Rudimentary error handling in #p4graph function. |
||
#12 | 6333 | Sam Stafford |
Big changes: 1) #p4graph parser function that generates GraphViz output (need the GraphViz plugin to see the graph on the page, or copy and paste the output into a GraphVis renderer) 2) Special:Perforce page (so that you can link to a graph on a separate page rather than embedding it) 3) #p4info parser function (mostly to provide content for Special:Perforce) There should also now be a change number embedded in the credits entry so you can see what version of the plugin you have installed. |
||
#11 | 6322 | Sam Stafford |
Change {{#p4changes}} function to return a <p4changes> tag rather than returning wiki-formatted output. Wiki syntax in change descriptions doesn't work out well, and this makes things simpler anyway. |
||
#10 | 6321 | Sam Stafford |
Make main function names consistent with each other. No functional change. |
||
#9 | 6320 | Sam Stafford |
New p4print tag/function. {{#p4print:path|raw/text/wiki}} raw (default) : Escapes all markup and displays in a <pre> block. text : Treats as wiki markup but adds a space to each line. wiki : Treats as wiki markup. <p4print path="//depot/file"/> Same as {{#p4print://depot/file}}. |
||
#8 | 6268 | Sam Stafford | Escape HTML passed as input to p4change tag. | ||
#7 | 6267 | Sam Stafford | Escape HTML in file paths. | ||
#6 | 6266 | Sam Stafford | Add more consistently named aliases for the tags and a parser function version of p4variants. | ||
#5 | 6264 | Sam Stafford | Fixed a typo. | ||
#4 | 6262 | Sam Stafford |
Escape HTML special chars in long change descriptions. There are other places where HTML tags could potentially mess up our output, but this is the one people are most likely to trip over accidentally. |
||
#3 | 6260 | Sam Stafford | New <p4variants> tag that dynamically lists branches with outstanding changes. | ||
#2 | 6195 | Sam Stafford | Update credits for Perforce MWiki plugins. | ||
#1 | 6192 | Sam Stafford |
A handful of Mediawiki extensions, skins, and whatnot. Not all of these are authored by me, but I want them all in one place and I need to keep track of any tweaks made. |