Perforce.php #48

  • //
  • guest/
  • sam_stafford/
  • mediawiki/
  • extensions/
  • Perforce/
  • Perforce.php
  • View
  • Commits
  • Open Download .zip Download (42 KB)
<?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("$IP/includes/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: 9449 $';
$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( 'p4diff2',    'parseP4Diff2'    );
    $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['p4diff2']    = array ( 0, 'p4diff2'    );
         $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;
	    $toPath = '';
	    $byUser = '';
	    $made = '';
	    if ( isSet( $args[1] ) && $args[1] != '//...' ) $toPath = ' to '.$args[1];
	    if ( isSet( $args[3] ) ) $byUser = ' by user '.$args[3];
	    if ( $toPath || $byUser ) $made = ' made';
	    if ( !isSet( $args[0] ) ) $args[0] = '40';
	    $wgOut->setPagetitle( 'Last '.$args[0].' changes'.$made.$toPath.$byUser );
	    $wgOut->addWikiText( call_user_func_array( 'getP4Changes', $args ) );
	}
	function execGraph( $args )
	{
	    global $wgOut;
	    if( substr( $args[0], 0, 2 ) != '//' ) $args[0] = '//' . $args[0];
	    $wgOut->setPagetitle( 'Graph of '.$args[0] );
	    $wgOut->addWikiText( call_user_func_array( 'getP4GraphText', $args ) );
	}
	function execInfo( $args )
	{
	    global $wgOut, $wgParser;
	    $wgOut->setPagetitle( 'Perforce server info' );
            $out = parseP4Info( $wgParser );
	    $wgOut->addWikiText( $out[0] );
	}
	function execJob( $args )
	{
	    global $wgOut;
	    $wgOut->setPagetitle( 'Perforce job '.$args[0].' '.$args[1] );
	    $wgOut->addWikiText( call_user_func_array( 'getP4Job', $args ) );
	}
	function execJobs( $args )
	{
	    global $wgOut;
	    $args = str_replace( '_', ' ', $args );
	    $wgOut->setPagetitle( 'Perforce jobs: '.$args[0] );
	    if ( !isSet( $args[0] ) ) $wgOut->setPagetitle( 'Perforce jobs' );
	    $wgOut->addWikiText( call_user_func_array( 'getP4Jobs', $args ) );

	}
	function execPrint( $args )
	{
	    global $wgOut;
	    $wgOut->setPagetitle( $args[0] );
	    $wgOut->addWikiText( call_user_func_array( 'getP4Print', $args ) );
	}
	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 = '' )
{
    return array( getP4Changes( $num, $path, $desc, $user ), 'noparse' => false );
}

function getP4Changes( $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 $tag;
}

# {{#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();
	}
    }

    // mock up specdef for getSpecFields()

    $output = array();
    $output[] = '... specdef change; ;;time; ;;user; ;;client; ;;status; ;;desc; ;;';

    // Get ze changes!

    $paths = explode( '+', $path );
    foreach( $paths as $path )
    {
	$cmdline = buildP4Cmd();
	$cmdline .= ' -Ztag -Zspecstring changes -l -m 10000 '.wfEscapeShellArg( $path );
	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 ( !isset( $chg['desc'] ) || 
                stripos( $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' );
}

# {{#p4diff2:path1|path2|flags}}
function parseP4Diff2( &$parser, $path1 = '', $path2 = '', $flags = '' )
{
    $parser->disableCache();

    if( $path1 == '' || $path2 == '' )
	return array( 'missing args for #p4diff2', 'noparse' => false );

    $quiet = '';
    $diff  = '-d';
    $add   = false;

    $regs = array();
    if( preg_match( '/-q/',      $flags, $regs ) ) $quiet = '-q ';
    if( preg_match( '/-A/',      $flags, $regs ) ) $add   = true;
    if( preg_match( '/(-d\w+)/', $flags, $regs ) ) $diff  = $regs[1];

    if( $add )
    {
	$diff .= 's';
	$quiet = '';
    }
    if( $diff == '-d' ) $diff = '';
    else $diff = wfEscapeShellArg( $diff ).' ';

    $cmdline = buildP4Cmd();
    $cmdline .= ' diff2 '.$quiet.$diff;
    $cmdline .= wfEscapeShellArg( $path1 ).' ';
    $cmdline .= wfEscapeShellArg( $path2 ).' 2>&1';

    $out = array();
    exec( $cmdline, $out );

    if( $add )
    {
	$count = 0;
	foreach( $out as $line )
	{
	    if( preg_match( '/add \d+ chunks (\d+) lines/', $line, $regs ) )
		$count += $regs[1];
	    if( preg_match( '/chunks \d+ \/ (\d+) lines/', $line, $regs ) )
		$count += $regs[1];
	}
	return array( $count, 'noparse' => false );
    }
    else
	return array( ' '.implode( "\n ", $out ), 'noparse' => false );
} 

# {{#p4graph:path|constraint|server|dot}}
function parseP4Graph( &$parser, $path, $const= '', $server = '', $dot = '' )
{
    $parser->disableCache();

    return array( getP4GraphText( $path, $const, $server, $dot ), 'noparse' => false );
}

function getP4GraphText( $path, $const = '', $server = '', $dot = '' )
{
    global $wgP4DVER;
    $flag = '-1 ';
    if ( $wgP4DVER && $wgP4DVER < 2009.1 ) $flag = '';

    $rankdir = 'LR';
    if ( $const == 'file' ) $rankdir = 'TD';

    if ( substr( $path, 0, 2 ) != '//' ) $path = '//' . $path;

    $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 $out;
}

# {{#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 array($out, 'noparse' => false);
}

# {{#p4job:job|field|format}}
function parseP4Job( &$parser, $job = '', $field = '', $format = '' )
{
    $parser->disableCache();

    return array( getP4Job( $job, $field, $format ), 'noparse' => false );
}

function getP4Job( $job = '', $field = '', $format = '' )
{
    if ( $job == '' )
    {
        return '<b>Please specify a job.</b>';
    }

    global $wgP4WEBURL;
    if ( $field == '' )
    {
        return '['.$wgP4WEBURL.$job.'?ac=135 '.$job.']';
    }

    $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 $result;
}

# {{#p4jobs:expr|fields|maxjobs|format|
#           tableattr|trattr|tdattr|query1|action1|...}}
function parseP4Jobs( &$parser, $expr='', $fields='', $maxjobs='', 
                       $format='', $tableattr='', $trattr='', $tdattr='' )
{
    $parser->disableCache();

    $extra_args = array();
    $arg = 8; // number of standard args
    while( func_num_args() > $arg + 1 )
    {
	$extra_args[] = func_get_arg( $arg );
	$arg++;
	$extra_args[] = func_get_arg( $arg );
	$arg++;
    }

    return array( getP4Jobs( $expr, $fields, $maxjobs, $format, $tableattr, $trattr, $tdattr, $extra_args ),
			'noparse' => false );
}

function getP4Jobs( $expr='', $fields='', $maxjobs='', 
                       $format='', $tableattr='', $trattr='', $tdattr='', $extra_args=array() )
{
    if ( !$maxjobs ) $maxjobs = '20';
    $mjobarr = explode( ' ', $maxjobs );
    $mjobs = intval($mjobarr[0]);
    if ( $mjobs > 1000 || $mjobs < 1 ) $mjobs = 100;
    if( isset( $mjobarr[1] ) )
    {
	$altport = $mjobarr[1];
    }
    else
    {
	$altport = '';
    }

    $cmdline = buildP4Cmd( $altport );
    $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 = 0; 
    while ( count( $extra_args ) > $arg + 1 )
    {
        $rule = array();
        $rule['query']  = $extra_args[ $arg ];  $arg++;
        $rule['action'] = $extra_args[ $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 = '';
	    if( isset( $job[strtolower($f)] ) )
	    {
		$value = $job[strtolower($f)];
	    }
	    $rawValue = $value;
            formatField( $format.$job['@format'], $value, $f );
            if ( $rawValue == $value && $f == 'Job' && $altport == '' )
	    {
		$value = '{{#p4job:'.$value.'}}';
	    }

            $result .= '|'.$tdattr.'|';
            $result .= $value;
            $result .= "\n";
        }
        $result .= "\n";
    }
    $result .= '|}';

    return $result;
}
										
# {{#p4print:path|raw/text/wiki}}
function parseP4Print( &$parser, $path = '', $mode = 'raw' )
{
    $parser->disableCache();

    return array( getP4Print( $path, $mode ), 'noparse' => false );
}

function getP4Print( $path = '', $mode = 'raw' )
{
    if ( $mode == 'raw' )
    {
        return "<p4print path=\"$path\"/>";
    }

    $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 )
{
    $parser->disableCache();

    $text = '<pre>';
    $text .= htmlspecialchars( getP4Print( $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 . ' -s -Ztag integrate -n ' . escapeshellarg( $branch.'@'.$cnum.',@'.$cnum ) . ' ' . escapeshellarg( $path );
            $integs = array();
            exec( $cmdline, $integs );
            foreach( $integs as $integ )
            {
                if ( $integ == 'info1: action integrate' )
                {
                    $cvar = TRUE;
                    break;
                }
                $errors = array();
                if( preg_match( '/^error: (.*)/', $integ, $errors ) &&
                   !preg_match( '/already integrated\.$/', $integ ) )
                {
                    $html .= '<font color="red">'.$errors[1].'</font><br/>';
                }
            }
            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 )
    {
	$var = preg_split( '/;/', $field );
        $field = array_shift( $var );
    }

    $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( '!', '&#33;', str_replace( '|', '&#124;', $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 ( array_key_exists( $field, $row ) && 
		     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
#48 9449 Sam Stafford Automatically prepend '//' to path arg of #p4graph (and
Special:Perforce/graph page) if not supplied.  This is so that it's not
required to use a URL with "///" in it, which I am told is distasteful.
 Since the path arg should ALWAYS be a depot path (right?) this is a
safe adjustment to make.
#47 8268 Sam Stafford {{#p4diff2:}} parser function. 

Usage: {{#p4diff2:path1|path2|flags}}

Runs a normal diff2 command with the output returned in a wiki-style
block, optionally using the -d or -q flags, or just the total number of
added/changed lines if the special -A flag is used.
#46 8148 Sam Stafford #p4job(s) linking fixes:

1) Default P4Web link now includes fix info.
2) The #p4jobs table doesn't generate a link for the job name if your
formatting changed it.
#45 8079 Sam Stafford Fix rule-based styling in #p4jobs.
 The logic that handled "extra"
arguments didn't survive the restructuring I did earlier to fix
compatibility problems, but it seems to work now.
#44 8077 Sam Stafford Make maxJobs limit for #p4jobs: function 1000 instead of 100.
 100
remains the default if you provide an out-of-bounds value.
#43 8072 Sam Stafford The variants AJAX now snags errors and reports them.
 Useful since it's the only thing that uses P4CLIENT.
#42 8071 Sam Stafford Fix p4print parser function.
#41 8070 Sam Stafford Rework special pages to work with new Mediawiki version.
 Ugh.
#40 8037 Sam Stafford Fix strict warning in #p4chgcats.
#39 8036 Sam Stafford Disable automatic #p4job linking when using alt port, since the P4Web probably doesn't match the alt port.
 Luckily it's not hard to swap in this functionality with a simple template.
#38 8035 Sam Stafford Cheesy altport hook in #p4jobs: pass P4PORT as the second word in the
maxJobs field, e.g.:

{{#p4jobs:subsystem=foo|Job Description|20 play:1999}}

The alternate P4PORT must be set in $wgP4ALTPORTS, e.g.:

$wgP4ALTPORTS = array( 'play:1999', '1981' );
#37 8034 Sam Stafford Fix inadvertant rollback.
#36 8033 Sam Stafford Fixes to work in strict mode.
#35 8032 Sam Stafford Pull in changes from Marc, get the plugin working in my new environment.
#34 7984 Sam Stafford Make #p4chgcats function able to handle multiple path arguments,
separated by "+".

The incoming changelists aren't sorted, so under each category you
first get all the matching changes from the first path, then the second
path, etc.  Kinda messy if you don't have your paths specified as
ordered and non-overlapping change/date ranges.
#33 7824 Sam Stafford #p4chgcats: keywords are now case-insensitive.
#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.