diff --git a/Utils/Git/Author.php b/Utils/Git/Author.php index 8f1a353bf..f86cb9336 100644 --- a/Utils/Git/Author.php +++ b/Utils/Git/Author.php @@ -55,8 +55,8 @@ class Author */ public function __construct(string $name = '', string $email = '') { - $this->name = $name; - $this->email = $email; + $this->name = escapeshellarg($name); + $this->email = escapeshellarg($email); } /** diff --git a/Utils/Git/Branch.php b/Utils/Git/Branch.php index f82aa2d59..c1caf713c 100644 --- a/Utils/Git/Branch.php +++ b/Utils/Git/Branch.php @@ -46,7 +46,7 @@ class Branch */ public function __construct(string $name = '') { - $this->name = $name; + $this->setName($name); } /** @@ -59,7 +59,7 @@ class Branch */ public function setName(string $name) { - $this->name = $name; + $this->name = escapeshellarg($name); } /** diff --git a/Utils/Git/Commit.php b/Utils/Git/Commit.php index e9976444a..b77545b8c 100644 --- a/Utils/Git/Commit.php +++ b/Utils/Git/Commit.php @@ -102,13 +102,23 @@ class Commit */ public function __construct(string $id = '') { + $this->id = escapeshellarg($id); $this->author = new Author(); $this->branch = new Branch(); $this->tag = new Tag(); + } - if (!empty($id)) { - // todo: fill base info - } + /** + * Get commit id. + * + * @return string + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ + public function getId() + { + return $this->id; } /** @@ -121,6 +131,8 @@ class Commit */ public function addFile(string $path) { + $path = escapeshellarg($path); + if (!isset($this->files[$path])) { $this->files[$path] = []; } @@ -164,7 +176,7 @@ class Commit */ public function setMessage(string $message) { - $this->message = $message; + $this->message = escapeshellarg($message); } /** @@ -305,6 +317,21 @@ class Commit return $this->date ?? new \DateTime('now'); } + /** + * Set commit date. + * + * @param \DateTime $date Commit date + * + * @return void + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ + public function setDate(\DateTime $date) + { + $this->date = $date; + } + /** * Set commit repository. * diff --git a/Utils/Git/Repository.php b/Utils/Git/Repository.php index 42ae8094d..558e51e36 100644 --- a/Utils/Git/Repository.php +++ b/Utils/Git/Repository.php @@ -16,6 +16,7 @@ namespace phpOMS\Utils\Git; use phpOMS\System\File\PathException; +use phpOMS\Validation\Validator; /** * Repository class @@ -54,6 +55,14 @@ class Repository */ private $envOptions = []; + /** + * Current branch. + * + * @var Branch + * @since 1.0.0 + */ + private $branch = null; + /** * Constructor * @@ -64,6 +73,7 @@ class Repository */ public function __construct(string $path) { + $this->branch = $this->getActiveBranch(); $this->setPath($path); } @@ -109,13 +119,16 @@ class Repository $this->path = realpath($path); + if ($this->path === false || !Validator::startsWith($this->path, ROOT_PATH)) { + throw new PathException($path); + } + if (file_exists($this->path . '/.git') && is_dir($this->path . '/.git')) { $this->bare = false; - // Is this a bare repo? - } elseif (is_file($this->path . '/config')) { - $parse_ini = parse_ini_file($this->path . '/config'); + } elseif (is_file($this->path . '/config')) { // Is this a bare repo? + $parseIni = parse_ini_file($this->path . '/config'); - if ($parse_ini['bare']) { + if ($parseIni['bare']) { $this->bare = true; } } @@ -126,14 +139,14 @@ class Repository * * @param string $cmd Command to run * - * @return string + * @return array * * @throws \Exception * * @since 1.0.0 * @author Dennis Eichhorn */ - private function run(string $cmd) : string + private function run(string $cmd) : array { $cmd = Git::getBin() . ' ' . $cmd; $pipes = []; @@ -165,7 +178,37 @@ class Repository throw new \Exception($stderr); } - return $stdout; + return $this->parseLines($stdout); + } + + /** + * Parse lines. + * + * @param string $lines Result of git command + * + * @return array + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ + private function parseLines(string $lines) : array + { + $lines = preg_replace('/!\\t+/', '|', $lines); + $lines = preg_replace('!\s+!', ' ', $lines); + $lineArray = preg_split('/\\r\\n|\\r|\\n/', $lines); + + foreach ($lineArray as $key => $line) { + $lineArray[$key] = trim($line, ' |'); + + if (empty($line)) { + unset($lineArray[$key]); + } else { + $lineArray[$key] = $line; + } + } + + return $lineArray; + } /** @@ -191,10 +234,22 @@ class Repository */ public function status() : string { - return $this->run('status'); + return implode("\n", $this->run('status')); } - public function add($files = '*') + /** + * Files to add to commit. + * + * @param string|array $files Files to commit + * + * @return string + * + * @throws \Exception + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ + public function add($files = '*') : string { if (is_array($files)) { $files = '"' . implode('" "', $files) . '"'; @@ -202,7 +257,7 @@ class Repository throw new \Exception('Wrong type'); } - return $this->run('add ' . $files . ' -v'); + return implode("\n", $this->run('add ' . $files . ' -v')); } public function rm($files = '*', bool $cached = false) : string @@ -216,9 +271,20 @@ class Repository return $this->run('rm ' . ($cached ? '--cached ' : '') . $files); } - public function commit($msg = '', $all = true) : string + /** + * Commit files. + * + * @param Commit $commit Commit to commit + * @param bool $all Commit all + * + * @return string + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ + public function commit(Commit $commit, $all = true) : string { - return $this->run('commit ' . ($all ? '-av' : '-v') . ' -m' . escapeshellarg($msg)); + return implode("\n", $this->run('commit ' . ($all ? '-av' : '-v') . ' -m ' . escapeshellarg($commit->getMessage()))); } public function cloneTo(string $target) : string @@ -248,19 +314,41 @@ class Repository return $this->run('clone ' . $source . ' ' . $this->path); } + /** + * Clean. + * + * @param bool $dirs Directories? + * @param bool $force Force? + * + * @return string + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ public function clean(bool $dirs = false, bool $force = false) : string { - return $this->run('clean ' . ($force ? ' -f' : '') . ($dirs ? ' -d' : '')); + return implode("\n", $this->run('clean' . ($force ? ' -f' : '') . ($dirs ? ' -d' : ''))); } - public function createBranch(string $branch, bool $force = false) : string + /** + * Create local branch. + * + * @param Branch $branch Branch + * @param bool $force Force? + * + * @return string + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ + public function createBranch(Branch $branch, bool $force = false) : string { - return $this->run('branch ' . ($force ? '-D' : '-d') . ' ' . $branch); + return implode("\n", $this->run('branch ' . ($force ? '-D' : '-d') . ' ' . $branch->getName())); } public function getBranches() : array { - $branches = explode('\n', $this->run('branch')); + $branches = $this->run('branch'); foreach ($branches as $key => &$branch) { $branch = trim($branch); @@ -275,7 +363,7 @@ class Repository public function getBranchesRemote() : array { - $branches = explode("\n", $this->run('branch -r')); + $branches = $this->run('branch -r'); foreach ($branches as $key => &$branch) { $branch = trim($branch); @@ -288,120 +376,257 @@ class Repository return $branches; } + /** + * Get active Branch. + * + * @return string + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ public function getActiveBranch() : string { - $branches = $this->getBranches(); - $active = preg_grep('/^\*/', $branches); - reset($active); + if (!isset($branch)) { + $branches = $this->getBranches(); + $active = preg_grep('/^\*/', $branches); + reset($active); - return current($active); + $this->branch = new Branch(current($active)); + } + + return $this->branch; } - public function checkout($branch) : string + /** + * Checkout. + * + * @param Branch $branch Branch to checkout + * + * @return string + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ + public function checkout(Branch $branch) : string { - return $this->run('checkout ' . $branch); + $result = implode("\n", $this->run('checkout ' . $branch->getName())); + $this->branch = null; + + return $result; } - public function merge($branch) : string + /** + * Merge with branch. + * + * @param Branch $branch Branch to merge from + * + * @return string + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ + public function merge(Branch $branch) : string { - return $this->run('merge ' . $branch . ' --no-ff'); + return implode("\n", $this->run('merge ' . $branch->getName() . ' --no-ff')); } + /** + * Fetch. + * + * @return string + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ public function fetch() : string { - return $this->run('fetch'); + return implode("\n", $this->run('fetch')); } - public function addTag(string $tag, string $message = null) : string + /** + * Create tag. + * + * @param Tag $tag Tag to create + * + * @return string + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ + public function createTag(Tag $tag) : string { - return $this->run('tag -a ' . $tag . ' -m ' . escapeshellarg($message ?? $tag)); + return implode("\n", $this->run('tag -a ' . $tag->getName() . ' -m ' . escapeshellarg($tag->getMessage()))); } - public function getTags(string $pattern) : string + /** + * Get all tags. + * + * @param string $pattern Tag pattern + * + * @return Tag[] + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ + public function getTags(string $pattern = '') : array { - $tags = explode('\n', $this->run('tag -l ' . $pattern)); + $pattern = empty($pattern) ? ' -l ' . $pattern : ''; + $lines = $this->run('tag' . $pattern); + $tags = []; - foreach ($tags as $key => &$tag) { - $tag = trim($tag); - - if ($tag === '') { - unset($tags[$key]); - } + foreach ($lines as $key => $tag) { + $tags[$tag] = new Tag($tag); } return $tags; } - public function push(string $remote, string $branch) : string + /** + * Push. + * + * @param string $remote Remote repository + * @param Branch $branch Branch to pull + * + * @return string + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ + public function push(string $remote, Branch $branch) : string { - return $this->run('push --tags ' . $remote . ' ' . $branch); + $remote = escapeshellarg($remote); + + return implode("\n", $this->run('push --tags ' . $remote . ' ' . $branch->getName())); } - public function pull(string $remote, string $branch) : string + /** + * Pull. + * + * @param string $remote Remote repository + * @param Branch $branch Branch to pull + * + * @return string + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ + public function pull(string $remote, Branch $branch) : string { - return $this->run('pull ' . $remote . ' ' . $branch); - } - - public function log(string $format = null) : string - { - return !isset($format) ? $this->run('log') : $this->run('log --pretty=format:"' . $format . '"'); + $remote = escapeshellarg($remote); + + return implode("\n", $this->run('pull ' . $remote . ' ' . $branch->getName())); } + /** + * Set repository description. + * + * @param string $description Repository description + * + * @return void + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ public function setDescription(string $description) { file_put_contents($this->getDirectoryPath(), $description); } + /** + * Get repository description. + * + * @return string + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ public function getDescription() : string { return file_get_contents($this->getDirectoryPath() . '/description'); } + /** + * Set environment value. + * + * @param string $key Key + * @param string $value Value + * + * @return void + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ public function setEnv(string $key, string $value) { $this->envOptions[$key] = $value; } + /** + * Get commit by id. + * + * @param string $commit Commit id + * + * @return Commit + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ public function getCommit(string $commit) : Commit { - return $this->run('log --format=%B -n 1 ' . $commit); + $commit = escapeshellarg($commit); + $lines = $this->run('log show --name-only ' . $commit); + $count = count($lines); + + preg_match('/[0-9ABCDEFabcdef]{40}/', $lines[0], $matches); + $author = explode(':', $lines[1]); + $author = explode('<', trim($author[1])); + $date = explode(':', $lines[2]); + + $commit = new Commit($matches[0]); + $commit->setAuthor(new Author(trim($author[0]), rtrim($author[1], '>'))); + $commit->setDate(new \DateTime(trim($date[1]))); + $commit->setMessage($lines[3]); + $commit->setTag(new Tag()); + $commit->setRepository($this); + $commit->setBranch($this->branch); + + for ($i = 4; $i < $count; $i++) { + $commit->addFile($lines[$i]); + } + + return $commit; } /** - * Count Commits. + * Count commits. * - * @return string + * @param \DateTime $start Start date + * @param \DateTime $end End date + * + * @return array * * @since 1.0.0 * @author Dennis Eichhorn */ - public function getCommitsCount(\DateTime $start = null, \DateTime $end = null, Author $author = null) : int + public function getCommitsCount(\DateTime $start = null, \DateTime $end = null) : array { - $result = $this->normalizeResult($this->run('shortlog -s -n --all')); + if (!isset($start)) { + $start = new \DateTime('1970-12-31'); + } - return ['']; - } + if (!isset($end)) { + $end = new \DateTime('now'); + } - private function normalizeResult(string $result) : string - { - str_replace('\t', '|', trim($result)); - } + $lines = $this->run('shortlog -s -n --since="' . $start->format('Y-m-d') . '" --before="' . $end->format('Y-m-d') . '" --all'); + $commits = []; - /** - * Get commits by author. - * - * @param Author $author Commits by author - * @param \DateTime $start Commits from - * @param \DateTime $end Commits to - * - * @return string - * - * @since 1.0.0 - * @author Dennis Eichhorn - */ - public function getCommitsBy(Author $author, \DateTime $start = null, \DateTime $end) : array - { - return $this->run('git log --before="' . $end->format('Y-m-d') . '" --after="' . $start->format('Y-m-d') . '" --author="' . $author->getName() . '" --reverse --pretty=format:"%cd %h %s" --date=short'); + foreach ($lines as $line) { + $count = explode('|', $line); + $commits[$count[1]] = $count[0]; + } + + return $commits; } /** @@ -417,6 +642,50 @@ class Repository return $this->run('config --get remote.origin.url'); } + /** + * Get commits by author. + * + * @param \DateTime $start Commits from + * @param \DateTime $end Commits to + * @param Author $author Commits by author + * + * @return Commit[] + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ + public function getCommitsBy(\DateTime $start = null, \DateTime $end = null, Author $author = null) : array + { + if (!isset($start)) { + $start = new \DateTime('1970-12-31'); + } + + if (!isset($end)) { + $end = new \DateTime('now'); + } + + if (!isset($author)) { + $author = ''; + } else { + $author = ' --author="' . $author->getName() . '"'; + } + + $lines = $this->run('git log --before="' . $end->format('Y-m-d') . '" --after="' . $start->format('Y-m-d') . '"' . $author . ' --reverse --date=short'); + $count = count($lines); + $commits = []; + + for ($i = 0; $i < $count; $i++) { + $match = preg_match('/[0-9ABCDEFabcdef]{40}/', $lines[$i], $matches); + + if ($match !== false && $match !== 0) { + $commit = $this->getCommit($matches[0]); + $commits[$commit->getId()] = $commit; + } + } + + return $commits; + } + /** * Get newest commit. * @@ -425,8 +694,12 @@ class Repository * @since 1.0.0 * @author Dennis Eichhorn */ - public function getNewest() : string + public function getNewest() : Commit { - return $this->run('log --name-status HEAD^..HEAD'); + $lines = $this->run('log --name-status HEAD^..HEAD'); + + preg_match('[0-9ABCDEFabcdef]{40}', $lines[0], $matches); + + return $this->getCommit($matches[0]); } } diff --git a/Utils/Git/Tag.php b/Utils/Git/Tag.php index a27c1b1c2..2ed24a7ad 100644 --- a/Utils/Git/Tag.php +++ b/Utils/Git/Tag.php @@ -28,5 +28,72 @@ namespace phpOMS\Utils\Git; */ class Tag { + /** + * Name. + * + * @var string + * @since 1.0.0 + */ + private $name = ''; + + /** + * Message. + * + * @var string + * @since 1.0.0 + */ + private $message = ''; + + /** + * Constructor + * + * @param string $name Tag name/version + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ + public function __construct(string $name = '') + { + $this->name = escapeshellarg($name); + } + + /** + * Set tag name + * + * @param string $message Tag message + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ + public function setMessage(string $message) + { + $this->message = escapeshellarg($message); + } + + /** + * Get tag message + * + * @return string + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ + public function getMessage() : string + { + return $this->message; + } + + /** + * Get tag name + * + * @return string + * + * @since 1.0.0 + * @author Dennis Eichhorn + */ + public function getName() : string + { + return $this->name; + } } \ No newline at end of file