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();