From ee96c5c7484a997de4c974ec94b78730800e57e1 Mon Sep 17 00:00:00 2001 From: Mariano Peterson Date: Tue, 22 Feb 2011 07:17:19 -0800 Subject: [PATCH] Pass unrecognized CLI options to svn. Enabled stop on copy. Renamed options to prevent naming conflicts with SVN, so that options can be passed through directly to SVN. This allows the username to be specified on the command line and enables the user to set any other SVN params. Changed: -s (--reverse) to -R (--reverse) -r (--repo) to -o (--repo) -v (--verbose) to -D (--debug) Only show history since current branch was created. This is another way to naturally limit the size of the svn response and improve performance. (Use --follow-copies to see full history with commits from origin branch.) Disable limit on number of commits transferred by setting --limit=0. Disable limit on days of data transferred by setting --days=0. (default) Refactored: Moved code for determining repo based on CWD from command line runner to Slog. Moved code for loading the formatter class from command line runner to Slog. --- CliParser.php | 23 +++++ Slog.php | 242 +++++++++++++++++++++++++++++++++++++++++++++----- cli.php | 95 ++++++++++---------- 3 files changed, 290 insertions(+), 70 deletions(-) diff --git a/CliParser.php b/CliParser.php index a9781d4..50b2154 100644 --- a/CliParser.php +++ b/CliParser.php @@ -36,6 +36,13 @@ class CliParser */ private $opts = array(); + /** + * Array of unrecognized args (no matching entry in $this->spec). + * + * @var array + */ + private $unrecognized = array(); + /** * Map of specifications for allowable parameters, keyed by short name. * @@ -124,23 +131,30 @@ public function load(array $argv) $nextArg = ($i + 1 < $argc) ? $argv[$i + 1] : null; if (strpos($arg, '=') !== false) { // -a=foo, --author=foo + $unrecognized = $arg; // raw value that will be added to the + // unrecognized list if we can't find it + // in $this->spec $tmp = explode('=', $arg); $name = trim(trim($tmp[0]), '-'); $value = trim($tmp[1]); } elseif ($nextArg && substr($nextArg, 0, 1) != '-') { // -a foo, --author foo + $unrecognized = $arg . " " . $nextArg; $name = trim(trim($arg), '-'); $value = trim($nextArg); $i++; } else { // -v, --verbose + $unrecognized = $arg; $name = trim(trim($arg), '-'); $value = null; } $this->debug("load(): analyzing input \"$name\"=\"$value\""); + $matched = false; foreach ($this->spec as $spec) { $this->debug("load(): comparing to option ({$spec['short']}, {$spec['long']})"); if ($name == $spec['short'] || $name == $spec['long']) { $this->debug("load(): MATCHES ({$spec['short']}, {$spec['long']})"); + $matched = true; if ($spec['type'] == self::TYPE_FLAG) { $this->opts[$spec['short']] = true; } elseif ($spec['type'] == self::TYPE_ARRAY) { @@ -159,6 +173,10 @@ public function load(array $argv) break; } } + + if (!$matched) { + $this->unrecognized[] = trim($unrecognized); + } } return $this; } @@ -206,4 +224,9 @@ public function debug($msg) return; print "[DEBUG] $msg\n"; } + + public function getUnrecognizedOpts() + { + return implode(" ", $this->unrecognized); + } } diff --git a/Slog.php b/Slog.php index eb6e1e5..fe1797c 100644 --- a/Slog.php +++ b/Slog.php @@ -61,6 +61,23 @@ class Slog */ private $limit; + /** + * Whether or not to follow commit history beyond the current branch and + * into the origin branch. + * + * @var bool + */ + private $stopOnCopy; + + /** + * String of command line options to send to svn log. This is intended to + * store options that were unrecognized by Slog so that we can pass them + * along to svn log instead. + * + * @var string + */ + private $svnOpts; + /** * Path to the SVN binary. * @@ -76,22 +93,132 @@ class Slog private $formatter; /** - * @param string $repo Path to the SVN repo. - * @param int $days Fetch commits submitted within this number of days. * @param int $limit Max number of commits to fetch from the SVN repo. */ - public function __construct($repo, $days, $limit) + public function __construct() { $this->debug = false; $this->removeAuthors = array(); $this->mustMatchAuthors = array(); $this->mustMatchRegexes = array(); - $this->repo = $repo; - $this->days = $days; - $this->limit = $limit; + $this->repo = null; + $this->days = null; + $this->limit = null; + $this->stopOnCopy = true; + $this->svnOpts = ''; $this->svn = '/usr/bin/env svn'; } + /** + * Override the path to the SVN repo. + * + * @param string $path Filepath or URL to the SVN repo. + * + * @return Slog (supports fluent interface) + */ + public function setRepo($path) + { + $this->repo = $repoUrl; + return $this; + } + + /** + * Get repo for working copy at $path. + * + * @param string $path + * + * @return string|null URL for SVN repo associated with Working Copy at + * $path. + */ + public function getRepoFromWorkingCopy($path) + { + $repo = null; + $cmd = "{$this->svn} info 2>/dev/null"; + $stdOut = array(); + $exitCode = null; + $lastLine = exec($cmd, $stdOut, $exitCode); + foreach ($stdOut as $line) { + if (substr($line, 0, 5) == 'URL: ') { + $repo = substr($line, 5); + break; + } + } + // $repo will be null if svn couldn't get the repo URL for the + // working copy at $path + return $repo; + } + + /** + * Get path to SVN repo. + * If repo was set with $slog->setRepo($repo), then $repo is used. + * Otherwise if the CWD is a working copy, the working copy's repo is used. + * Otherwise if the environment variable SLOG_DEFAULT_REPO exists, that is used. + * If none of the above can be resolved, an exception is thrown. + * + * @throws Exception if none of the above methods are able to resolve the repo URL. + * + * @return string|null URL for SVN repo. + */ + public function getRepo() + { + if (empty($this->repo)) { + $this->repo = $this->getRepoFromWorkingCopy(getcwd()); + } + if (empty($this->repo)) { + if (isset($_SERVER['SLOG_DEFAULT_REPO'])) { + $this->repo = $_SERVER['SLOG_DEFAULT_REPO']; + } + } + if (empty($this->repo)) { + throw new Exception("Could not determine the SVN repo. Its likely " + . "that the current directory is not a working copy."); + } + return $this->repo; + } + + /** + * Set the maximum number of days worth of historical data to fetch from svn log. + * + * @param int $days Maximum number of days worth of historical data to + * fetch from svn log. + * + * @return Slog (supports fluent interface) + */ + public function setDays($days) + { + $this->days = (int)$days; + return $this; + } + + /** + * @return int Number of days of historical data to fetch from svn log. + */ + public function getDays() + { + return $this->days; + } + + /** + * Set the max number of commits to fetch from the SVN server. + * + * @param int $limit Max number of commits to fetch from the SVN server. + * + * @return Slog (supports fluent interface) + */ + public function setLimit($limit) + { + $this->limit = (int) $limit; + return $this; + } + + /** + * @return int Max number of commits to fetch from the SVN server. + */ + public function getLimit() + { + return $this->limit; + } + /** * @param bool $status Set true to enable debugging output. * @@ -120,20 +247,46 @@ private function debug($msg) print "[DEBUG] $msg\n"; } } - - private function load($repo, $days, $limit) + + /** + * Set whether or not to follow commits past the current branch to the origin. + * + * @param bool $bool Set true to only show commits from the current branch, + * set false to show commits all the way back to origin. + * + * @return Slog (supports fluent interface) + */ + public function setStopOnCopy($bool = true) { - $startDate = date(self::DATE_FORMAT, strtotime("-$days days")); - $cmd = sprintf( - "%s log --xml -r {%s}:HEAD -v %s 2>&1", - $this->svn, - $startDate, - $repo - ); - if ($limit) { - $cmd .= " --limit $limit"; + $this->stopOnCopy = (bool)$bool; + return $this; + } + + /** + * Returns true if the Slog instance is configured to stop on copy. + * + * @return bool True if the Slog instance is configured to stop on copy. + */ + public function getStopOnCopy() + { + return $this->stopOnCopy; + } + + private function load() + { + if ($this->getDays()) { + $startDate = date(self::DATE_FORMAT, strtotime("-{$this->getDays()} days")); + $revision = "--revision {{$startDate}}:HEAD"; + } else { + $revision = ''; } - $this->debug("Executing cmd: $cmd"); + $limit = $this->getLimit() ? "--limit {$this->getLimit()}" : ''; + $stopOnCopy = $this->getStopOnCopy() ? "--stop-on-copy" : ''; + $repo = $this->getRepo(); + + $cmd = "{$this->svn} log {$this->svnOpts} --xml {$revision} {$limit} {$stopOnCopy} -v {$repo} 2>&1"; + + $this->debug("Executing command: {$cmd}"); $xml = $this->fetchXml($cmd); $this->data = simplexml_load_string($xml); } @@ -234,7 +387,7 @@ public function toString() public function __toString() { try { - $this->load($this->repo, $this->days, $this->limit); + $this->load(); } catch (Exception $e) { $msg = sprintf( "\nERROR(%s): %s\n\nSTACK TRACE:\n%s\n\n", $e->getCode(), @@ -284,9 +437,33 @@ private function filter($commit) } return true; } - + + /** + * Loads and sets the formatter to be used for output, based on + * a short name and a cli object (for style preferences). + * + * @param string $name Short name of the Formatter class to load. + * @param CliParser $cli Command line options to use (for style preferences) + * + * @return Slog (supports fluent interface) + */ + public function setFormatterByName($name, CliParser $cli) + { + $format = ucfirst(strtolower($name)); + $class = 'Slog_Formatter_' . $format; + $file = dirname(__FILE__) . "/Slog/Formatter/{$format}.php"; + if (!is_readable($file)) { + throw new Exception("Could not load formatter: $format. Tried to load from $file."); + } + require_once($file); + $formatter = new $class($cli); + $this->setFormatter($formatter); + return $this; + } + /** * Set the formatter to be used by the log printer. + * @see setFormatterByName() * * @param SvnLog_Formatter_Interface $formatter Formatting implementation * to use for printing the @@ -309,5 +486,28 @@ public function getFormatter() { return $this->formatter; } - + + /** + * Set string of additional command line opts to send to svn log. + * + * @param string $opts String of additional command line opts to send to svn log. + * + * @return Slog (supports fluent interface) + */ + public function setSvnOpts($opts) + { + $this->svnOpts = $opts; + return $this; + } + + /** + * Returns the string of additional command line opts to send to svn log. + * + * @return String of additional command line opts to send to svn log. + */ + public function getSvnOpts() + { + return $this->svnOpts; + } + } diff --git a/cli.php b/cli.php index 23290fc..e936ffa 100755 --- a/cli.php +++ b/cli.php @@ -4,36 +4,46 @@ require_once(dirname(__FILE__) . '/CliParser.php'); require_once(dirname(__FILE__) . '/Slog/Formatter/Interface.php'); -define("DEFAULT_DAYS", 180); -define("DEFAULT_LIMIT", 2000); // set null to remove limit +define("DEFAULT_DAYS", 0); //default is no limit +define("DEFAULT_LIMIT", 2000); $cli = new CliParser(); -$cli->about('Wrapper around the SVN command line tool that provides ' +$cli->about('Wrapper around the svn log command line tool that provides ' . 'additional filtering capabilities such as ' . 'filtering by regex and author.') ->addOpt('a', 'author', 'Only show commits written by author. ' . 'Set repeatedly to cast a larger net.', false, CliParser::TYPE_ARRAY, ',') ->addOpt('c', 'color', 'Use color to style output.', false, CliParser::TYPE_FLAG) - ->addOpt('d', 'days', 'Number of days of data to fetch. ' - . 'Default is ' . DEFAULT_DAYS . '.') - ->addOpt('e', 'regex', "Only show commits that match regex. Set " - . "repeatedly to cast a larger net.\n" - . "e.g., /component/i") + ->addOpt('d', 'days', 'Number of days of data to fetch. Set --days=0 to ' + . 'remove limit. Default is ' . DEFAULT_DAYS . '.') + ->addOpt('D', 'debug', 'Print debugging information.', false, CliParser::TYPE_FLAG) + ->addOpt('e', 'regex', "Only show commits that match regex " + . "(e.g., /component/i). Set repeatedly to " + . "cast a larger net.") ->addOpt('f', 'format', "Format type: summary, shortsummary, oneline.\n" . "Formats are defined in " . dirname(__FILE__) . "/Slog/Formatter/*") + ->addOpt('F', 'follow-copies', "Follow copies (i.e., follow the commit " + . "history all the way to the origin branch). Default is " + . "is to only follow commits on the current branch.", false, CliParser::TYPE_FLAG) ->addOpt('h', 'help', 'Show usage.', false, CliParser::TYPE_FLAG) ->addOpt('i', 'ignore', 'Exclude commits written by author. ' . 'Set this repeatedly to cast a larger net.', false, CliParser::TYPE_ARRAY, ',') - ->addOpt('l', 'limit', 'Limit the number of commits to search. ' - . 'Default is ' . DEFAULT_LIMIT . '.') - ->addOpt('r', 'repo', "SVN repository, e.g., http://svn.host.com/project.\n" + ->addOpt('l', 'limit', 'Limit the number of commits that are transferred ' + . 'from the server (default is ' . DEFAULT_LIMIT . '). ' + . 'This improves performance by limiting the amount of ' + . 'data that SVN sends over. However, note that filters ' + . 'like --regex and --author are evaluated on the limited ' + . 'commit log that it is received from the server. This ' + . 'means that setting the limit to 10 and then also ' + . 'applying an --author filter is likely to print less ' + . 'than 10 commits. Set --limit=0 to remove the limit.') + ->addOpt('o', 'repo', "SVN repository, e.g., http://svn.host.com/project.\n" . "Otherwise if the current directory is an SVN working " . "copy, the working copy's repo will be used.\n" . "Otherwise if the environment variable SLOG_REPO is " . "set, the SLOG_REPO variable will be used.") - ->addOpt('s', 'reverse', 'Print the most recent commits first.', false, CliParser::TYPE_FLAG) - ->addOpt('v', 'verbose', 'Print debugging information.', false, CliParser::TYPE_FLAG) + ->addOpt('R', 'reverse', 'Print the most recent commits first.', false, CliParser::TYPE_FLAG) ->load($argv); if ($cli->get('help')) { @@ -41,61 +51,48 @@ exit(0); } - -// Get repo for current directory /////////////// -$repo = null; -$cmd = "/usr/bin/env svn info 2>/dev/null"; -$stdOut = array(); -$exitCode = null; -$lastLine = exec($cmd, $stdOut, $exitCode); -foreach ($stdOut as $line) { - if (substr($line, 0, 5) == 'URL: ') { - $repo = substr($line, 5); - break; - } +$slog = new Slog(); +if ($cli->get('repo')) { + $slog->setRepo($cli->get('repo')); } -if (!$repo && isset($_SERVER['SLOG_DEFAULT_REPO'])) { - $repo = $_SERVER['SLOG_DEFAULT_REPO']; +if ($cli->get('limit') === null) { + $slog->setLimit(DEFAULT_LIMIT); +} elseif ($cli->get('limit') != 0) { + $slog->setLimit($cli->get('limit')); } - -if (!$repo && !$cli->get('repo')) { - print "Could not determine the SVN repo. Run this again inside an\n" - . "SVN checkout or specify the repo from the command line.\n\n"; - exit(1); +if ($cli->get('days' === null)) { + $slog->setLimit(DEFAULT_DAYS); +} elseif ($cli->get('days') != 0) { + $slog->setDays($cli->get('days')); } +<<<<<<< HEAD // END: get repo //////////////////////////////// $slog = new Slog($cli->get('repo', $repo), $cli->get('days', DEFAULT_DAYS), $cli->get('limit', DEFAULT_LIMIT)); -if ($cli->get('verbose')) { +======= +>>>>>>> Pass unrecognized CLI options to svn. Enabled stop on copy. +if ($cli->get('debug')) { $slog->setDebug(true); } - if ($cli->get('author')) { $slog->matchAuthor($cli->get('author')); } if ($cli->get('ignore')) { + // Use this to filter out bot commits, i.e., CSS minifiers etc. $slog->removeCommitsFromAuthor($cli->get('ignore')); } if ($cli->get('regex')) { $slog->matchRegex($cli->get('regex')); } - -// Get the formatter -$format = ucfirst(strtolower($cli->get('format', 'summary'))); -$formatClass = 'Slog_Formatter_' . $format; -$formatFile = dirname(__FILE__) . '/Slog/Formatter/' . $format . '.php'; -if (!is_readable($formatFile)) { - print "Could not load formatter: $format.\n"; - exit(1); +if ($cli->get('follow-copies')) { + $slog->setStopOnCopy(false); } -require_once($formatFile); -$formatter = new $formatClass($cli); -$slog->setFormatter($formatter); -// END: get formatter - +if ($cli->getUnrecognizedOpts()) { + // Pass any unrecognized options along to svn log + $slog->setSvnOpts($cli->getUnrecognizedOpts()); +} +$slog->setFormatterByName($cli->get('format', 'summary'), $cli); -// Remove bot entries if desired -// $log->removeCommitsFromAuthor("some-bot-name"); print $slog->toString();