<?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;
       }
   }
}
?>