├── composer.json └── src ├── AbstractGit.php ├── Attributes ├── ConfigKey.php ├── ConfigSectionName.php └── ConfigValue.php ├── Backup ├── AbstractBackup.php ├── ArrayBackupElementDict.php ├── Backup.php └── Elements │ ├── AbstractBackupElement.php │ ├── ConfigBackupElement.php │ ├── HookBackupElement.php │ └── UntrackedFilesBackupElement.php ├── CachedGit.php ├── Command ├── Commands │ ├── AbstractCommand.php │ ├── ArchiveCommand.php │ ├── AttributeCommand.php │ ├── BranchCommand.php │ ├── CommitCommand.php │ ├── ConfigCommand.php │ ├── FetchCommand.php │ ├── FileCommand.php │ ├── GarbageCommand.php │ ├── GrepCommand.php │ ├── HelpCommand.php │ ├── HookCommand.php │ ├── IgnoreCommand.php │ ├── IndexCommand.php │ ├── LogCommand.php │ ├── MergeCommand.php │ ├── PathCommand.php │ ├── PullCommand.php │ ├── PushCommand.php │ ├── RemoteCommand.php │ ├── SetupCommand.php │ ├── StashCommand.php │ ├── StatusCommand.php │ ├── SubmoduleCommand.php │ └── TagCommand.php └── GitCommandBuilder.php ├── Concerns └── SwitchFolder.php ├── Config ├── Configurators │ ├── AliasConfigurator.php │ ├── BranchConfigurator.php │ ├── CommitConfigurator.php │ ├── CoreConfigurator.php │ ├── CredentialConfigurator.php │ ├── DiffConfigurator.php │ ├── InstawebConfigurator.php │ ├── PackConfigurator.php │ ├── SubmoduleConfigurator.php │ └── UserConfigurator.php ├── ConfiguratorsDict.php ├── RegexConfigResultParser.php └── Subjects │ ├── AbstractSubject.php │ ├── Alias.php │ ├── AliasList.php │ ├── Branch.php │ ├── BranchList.php │ ├── ConfigCommit.php │ ├── ConfigDiff.php │ ├── ConfigSubmodule.php │ ├── ConfigSubmoduleList.php │ ├── Core.php │ ├── Credential.php │ ├── Instaweb.php │ ├── Pack.php │ ├── SubjectsCollection.php │ └── User.php ├── Contracts ├── Attribute │ └── GitAttribute.php ├── AuthorHydrator.php ├── Backup │ ├── BackupElement.php │ ├── BackupElementDict.php │ └── GitBackup.php ├── Commands │ ├── FolderSwitchable.php │ ├── GitArchiveCommand.php │ ├── GitAttributeCommand.php │ ├── GitBranchCommand.php │ ├── GitCommitCommand.php │ ├── GitConfigCommand.php │ ├── GitFetchCommand.php │ ├── GitFileCommand.php │ ├── GitGarbageCommand.php │ ├── GitGrepCommand.php │ ├── GitHelpCommand.php │ ├── GitHookCommand.php │ ├── GitIgnoreCommand.php │ ├── GitIndexCommand.php │ ├── GitLogCommand.php │ ├── GitMergeCommand.php │ ├── GitPathCommand.php │ ├── GitPullCommand.php │ ├── GitPushCommand.php │ ├── GitRemoteCommand.php │ ├── GitSetupCommand.php │ ├── GitStashCommand.php │ ├── GitStatusCommand.php │ ├── GitSubmoduleCommand.php │ ├── GitTagCommand.php │ ├── HasSubmodules.php │ └── RefsPacker.php ├── Common │ └── Arrayable.php ├── Config │ ├── ConfigAttribute.php │ ├── ConfigResultParser.php │ ├── ConfigSubject.php │ ├── ConfigSubjectList.php │ └── SubjectConfigurator.php ├── Factory │ └── GitHandlerFactory.php ├── Handler │ ├── GitHandler.php │ ├── HasAttributes.php │ ├── HasBackup.php │ ├── HasRemotes.php │ └── Versionable.php ├── HasContext.php ├── HasUrl.php ├── Log │ ├── LogQuery.php │ ├── LogQueryAction.php │ └── LogQueryBuilder.php ├── LogParser.php ├── Origin │ ├── OriginDownloader.php │ └── OriginUrlBuilder.php ├── PathGenerator.php ├── PushSetup.php └── Transaction │ ├── GitTransaction.php │ └── TransactionOperation.php ├── Data ├── Author.php ├── Author │ ├── CacheableHydrator.php │ └── Hydrator.php ├── Commit.php ├── CommitsAuthor.php ├── FileMatch.php ├── GitAttributes.php ├── GitContext.php ├── Hook.php ├── Log.php ├── LogCollection.php ├── Remotes.php ├── Repo.php ├── Stash.php ├── Submodule.php ├── Tag.php └── Version.php ├── Enum ├── ArchiveFormat.php ├── BranchBadName.php ├── ConfigSectionName.php ├── Enumerable.php ├── FormatPlaceholder.php ├── GarbageCollectMode.php ├── HookName.php ├── ResetMode.php └── StatusResult.php ├── Exceptions ├── AlreadySwitched.php ├── BadRevision.php ├── BranchAlreadyExists.php ├── BranchDoesNotHaveCommits.php ├── BranchHasNoUpstream.php ├── BranchNotFound.php ├── CannotMergeAbort.php ├── CannotMergeException.php ├── ConfigDataNotFound.php ├── ConfigSectionNotFound.php ├── ConfigVariableNotFound.php ├── FileNotFound.php ├── GitHandlerException.php ├── GivenInvalidUri.php ├── HookNotExists.php ├── MergeHeadMissing.php ├── NotSomethingWeCanMerge.php ├── NothingToCommit.php ├── ObjectNameNotValid.php ├── OriginUrlNotFound.php ├── PathAlreadyExists.php ├── PathIncorrect.php ├── PathIsDirectoryNotCould.php ├── PreviousCherryPickIsNowEmpty.php ├── ReferenceInvalid.php ├── RemoteAlreadyExists.php ├── RemoteNotFilled.php ├── RemoteNotFound.php ├── RemoteRepositoryNotFound.php ├── RepositoryAlreadyExists.php ├── StashDoesNotExists.php ├── SubjectConfiguratorNotFound.php ├── SubmoduleNotFound.php ├── TagAlreadyExists.php ├── TagNotFound.php ├── UnexpectedException.php └── UnknownRevisionInWorkingTree.php ├── Factory ├── CachedGitFactory.php ├── DownloaderFactory.php └── LocalGitFactory.php ├── Files ├── AttributesFile.php └── SubmodulesFile.php ├── Git.php ├── Making └── MakingPush.php ├── Origin ├── Downloader.php └── Url │ ├── AbstractOriginUrlBuilder.php │ ├── BitbucketOriginUrlBuilder.php │ ├── GitUrl.php │ ├── GithubOriginUrlBuilder.php │ ├── GitlabOriginUrlBuilder.php │ └── OriginUrlSelector.php ├── Support ├── Chmod.php ├── LocalFileSystem.php ├── LogBuilder.php ├── Logger.php ├── SimpleHttpClient.php ├── TemporaryPathGenerator.php ├── ToArray.php ├── TypeCaster.php └── Uri.php └── Transactions └── ArchiveTransaction.php /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "artarts36/git-handler", 3 | "description": "Git Handler", 4 | "type": "library", 5 | "require": { 6 | "php": ">=7.3", 7 | "artarts36/shell-command": "^2.1.13", 8 | "artarts36/str": "^2.0.1", 9 | "artarts36/file-system-contracts": "0.2.0", 10 | "psr/http-client": "^1.0", 11 | "guzzlehttp/psr7": "^1.8 | ^2", 12 | "artarts36/local-file-system": "^0.1.1" 13 | }, 14 | "require-dev": { 15 | "phpunit/phpunit": "^9.3", 16 | "squizlabs/php_codesniffer": "^3.5", 17 | "guzzlehttp/guzzle": "^7.3", 18 | "infection/infection": "^0.18.2", 19 | "composer/package-versions-deprecated": "1.11.99.2", 20 | "phpstan/phpstan": "^0.12.96", 21 | "jetbrains/phpstorm-attributes": "^1.0", 22 | "roave/security-advisories": "dev-latest", 23 | "phpdocumentor/reflection": "^5.3" 24 | }, 25 | "license": "MIT", 26 | "authors": [ 27 | { 28 | "name": "ArtARTs36", 29 | "email": "temicska99@mail.ru" 30 | } 31 | ], 32 | "autoload": { 33 | "psr-4": { 34 | "ArtARTs36\\GitHandler\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "ArtARTs36\\GitHandler\\Tests\\": "tests/", 40 | "ArtARTs36\\GitHandler\\DocBuilder\\": "docs/DocBuilder" 41 | }, 42 | "files": [ 43 | "tests/functions.php" 44 | ] 45 | }, 46 | "config": { 47 | "allow-plugins": { 48 | "infection/extension-installer": true 49 | } 50 | }, 51 | "scripts": { 52 | "lint": [ 53 | "echo 'Check code on PSR'", 54 | "./vendor/bin/phpcs --standard=PSR2 src/", 55 | "./vendor/bin/phpcs --standard=PSR2 tests/" 56 | ], 57 | "stat-analyse": [ 58 | "echo 'Run stat analyse'", 59 | "./vendor/bin/phpstan analyse -l 5 src" 60 | ], 61 | "test": [ 62 | "echo 'Run tests'", 63 | "XDEBUG_MODE=coverage ./vendor/bin/phpunit -v --coverage-text --configuration phpunit.xml --coverage-clover=tests/reports/logs/clover.xml --coverage-xml=tests/reports/logs/coverage-xml --log-junit=tests/reports/logs/junit.xml" 64 | ], 65 | "mutate-test": [ 66 | "echo 'Run mutation testing'", 67 | "./vendor/infection/infection/bin/infection --threads=4 --no-progress --min-covered-msi=76.5 --coverage=tests/reports/logs/" 68 | ], 69 | "build-docs": [ 70 | "echo 'Build documentation'", 71 | "php docs/DocBuilder/build.php" 72 | ], 73 | "check-docs-actual": [ 74 | "echo 'Check Documentation is actually'", 75 | "php docs/DocBuilder/is_actual.php" 76 | ], 77 | "build-changelog": [ 78 | "echo 'Build CHANGELOG.MD'", 79 | "php docs/DocBuilder/build_changelog.php" 80 | ] 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Attributes/ConfigKey.php: -------------------------------------------------------------------------------- 1 | key = $key; 15 | } 16 | 17 | public function __toString(): string 18 | { 19 | return $this->key; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Attributes/ConfigSectionName.php: -------------------------------------------------------------------------------- 1 | git = $git; 31 | $this->files = $files; 32 | $this->building = $building; 33 | } 34 | 35 | public function dump(string $path): void 36 | { 37 | $this->doDump($path, $this->building); 38 | } 39 | 40 | public function dumpOnly(string $path, array $elements): void 41 | { 42 | $this->doDump($path, $this->building->only($elements)); 43 | } 44 | 45 | public function restore(string $path): void 46 | { 47 | $this->doRestore($path, $this->building); 48 | } 49 | 50 | public function restoreOnly(string $path, array $elements): void 51 | { 52 | $this->doRestore($path, $this->building->only($elements)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Backup/ArrayBackupElementDict.php: -------------------------------------------------------------------------------- 1 | $elements 14 | */ 15 | public function __construct(array $elements) 16 | { 17 | $this->elements = $elements; 18 | } 19 | 20 | /** 21 | * @return iterable 22 | */ 23 | public function getIterator(): iterable 24 | { 25 | return new \ArrayIterator($this->elements); 26 | } 27 | 28 | public function get(array $classes): array 29 | { 30 | $elems = []; 31 | 32 | foreach ($this->elements as $element) { 33 | if (in_array($element->identity(), $classes) || in_array(get_class($element), $classes)) { 34 | $elems[] = $element; 35 | } 36 | } 37 | 38 | return $elems; 39 | } 40 | 41 | public function only(array $classes): self 42 | { 43 | $elements = $this->get($classes); 44 | 45 | if (count($elements) === 0) { 46 | throw new \LogicException('Not found workflow elements'); 47 | } 48 | 49 | return new self($elements); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Backup/Backup.php: -------------------------------------------------------------------------------- 1 | files->getFileContent($path)); 13 | 14 | foreach ($dict->get(array_keys($dumpMap)) as $element) { 15 | $element->restore($this->git, $dumpMap[get_class($element)]); 16 | } 17 | } 18 | 19 | protected function doDump(string $path, BackupElementDict $dict): void 20 | { 21 | $dumpMap = []; 22 | 23 | foreach ($dict as $element) { 24 | $dumpMap[get_class($element)] = $element->dump($this->git); 25 | } 26 | 27 | $this->files->createFile($path, serialize($dumpMap)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Backup/Elements/AbstractBackupElement.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | #[ArrayShape(['content' => 'string'])] 17 | public function dump(GitHandler $git): array 18 | { 19 | return [ 20 | 'content' => $git->files()->getContent('.git/config'), 21 | ]; 22 | } 23 | 24 | /** 25 | * @param array $data 26 | */ 27 | public function restore( 28 | GitHandler $git, 29 | #[ArrayShape(['content' => 'string'])] 30 | array $data 31 | ): void { 32 | $git->files()->createFile('.git/config', $data['content']); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Backup/Elements/HookBackupElement.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function dump(GitHandler $git): array 18 | { 19 | return $git->hooks()->getAll(); 20 | } 21 | 22 | /** 23 | * @param array $data 24 | */ 25 | public function restore(GitHandler $git, array $data): void 26 | { 27 | $hooks = $git->hooks(); 28 | 29 | foreach ($data as $hook) { 30 | $hooks->add(HookName::from($hook->name), $hook->script); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Backup/Elements/UntrackedFilesBackupElement.php: -------------------------------------------------------------------------------- 1 | files(); 16 | 17 | foreach ($git->statuses()->getUntrackedFiles() as $file) { 18 | $map[$file] = $manager->getContent($file); 19 | } 20 | 21 | return $map; 22 | } 23 | 24 | /** 25 | * @param array $data 26 | */ 27 | public function restore(GitHandler $git, array $data): void 28 | { 29 | $manager = $git->files(); 30 | 31 | foreach ($data as $path => $content) { 32 | $manager->createFile($path, $content); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Command/Commands/AbstractCommand.php: -------------------------------------------------------------------------------- 1 | builder = $builder; 20 | $this->executor = $executor; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Command/Commands/ArchiveCommand.php: -------------------------------------------------------------------------------- 1 | files = $files; 31 | $this->context = $context; 32 | 33 | parent::__construct($builder, $executor); 34 | } 35 | 36 | public function create(string $path): void 37 | { 38 | $this 39 | ->builder 40 | ->make() 41 | ->addArgument('archive') 42 | ->addOptionWithValue('format', ArchiveFormat::from(pathinfo($path, PATHINFO_EXTENSION))->value) 43 | ->addOptionWithValue('output', $path) 44 | ->addArgument('HEAD') 45 | ->setExceptionTrigger(UserExceptionTrigger::fromCallbacks([ 46 | function (CommandResult $result) use ($path) { 47 | if ($result 48 | ->getError() 49 | ->contains("fatal: could not create archive file '$path': Is a directory")) { 50 | throw new PathIsDirectoryNotCould(); 51 | } 52 | } 53 | ])) 54 | ->executeOrFail($this->executor); 55 | } 56 | 57 | public function packRefs(string $path): void 58 | { 59 | $this 60 | ->builder 61 | ->toDir($this->files->downPath($this->context->getRefsDir()), 'tar') 62 | ->addCutOption('cf') 63 | ->addArgument($path) 64 | ->addArgument(pathinfo($this->context->getRefsDir(), PATHINFO_BASENAME)) 65 | ->executeOrFail($this->executor); 66 | } 67 | 68 | public function unpackRefs(string $path): void 69 | { 70 | $this 71 | ->builder 72 | ->toDir($this->context->getGitDir(), 'tar') 73 | ->addCutOption('xf') 74 | ->addArgument($path) 75 | ->executeOrFail($this->executor); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Command/Commands/AttributeCommand.php: -------------------------------------------------------------------------------- 1 | files = $files; 28 | $this->context = $context; 29 | $this->file = new AttributesFile(); 30 | $this->seeToRoot(); 31 | } 32 | 33 | public function add(string $pattern, array $attributes): void 34 | { 35 | $map = $this->isFileExists() ? $this->getMap() : []; 36 | 37 | $map[$pattern] = array_unique(array_merge($map[$pattern] ?? [], $attributes)); 38 | 39 | $this->saveFromMap($map); 40 | } 41 | 42 | public function find(string $pattern): ?GitAttributes 43 | { 44 | $map = $this->getMap(); 45 | 46 | if (! array_key_exists($pattern, $map)) { 47 | return null; 48 | } 49 | 50 | return new GitAttributes($pattern, $map[$pattern]); 51 | } 52 | 53 | public function delete(string $pattern): bool 54 | { 55 | $map = $this->getMap(); 56 | 57 | if (! array_key_exists($pattern, $map)) { 58 | return false; 59 | } 60 | 61 | unset($map[$pattern]); 62 | 63 | return $this->saveFromMap($map); 64 | } 65 | 66 | /** 67 | * @return array> 68 | * @throws \ArtARTs36\FileSystem\Contracts\FileNotFound 69 | */ 70 | public function getMap(): array 71 | { 72 | $content = $this->files->getFileContent($this->getPath()); 73 | 74 | return $this->file->buildMap($content); 75 | } 76 | 77 | /** 78 | * @codeCoverageIgnore 79 | */ 80 | public function getPath(): string 81 | { 82 | return $this->folder . DIRECTORY_SEPARATOR . '.gitattributes'; 83 | } 84 | 85 | /** 86 | * @param array> $map 87 | */ 88 | protected function saveFromMap(array $map): bool 89 | { 90 | return $this->files->createFile( 91 | $this->getPath(), 92 | count($map) === 0 ? '' : $this->file->buildContent($map) 93 | ); 94 | } 95 | 96 | final protected function isFileExists(): bool 97 | { 98 | return $this->files->exists($this->getPath()); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Command/Commands/BranchCommand.php: -------------------------------------------------------------------------------- 1 | builder 22 | ->make() 23 | ->addArgument('branch') 24 | ->addCutOption('d') 25 | ->addArgument($branch) 26 | ->setExceptionTrigger(UserExceptionTrigger::fromCallbacks([ 27 | function (CommandResult $result) use ($branch) { 28 | if ($result->getError()->contains("error: branch '$branch' not found")) { 29 | throw new BranchNotFound($branch); 30 | } 31 | } 32 | ])) 33 | ->executeOrFail($this->executor) 34 | ->getResult() 35 | ->contains("Deleted branch $branch"); 36 | } 37 | 38 | public function create(string $branch): void 39 | { 40 | $this 41 | ->builder 42 | ->make() 43 | ->addArgument('branch') 44 | ->addArgument($branch) 45 | ->setExceptionTrigger(UserExceptionTrigger::fromCallbacks([ 46 | function (CommandResult $result) use ($branch) { 47 | $already = $result->getError()->match("/fatal: A branch named '$branch' already exists/i"); 48 | if ($already->isNotEmpty()) { 49 | throw new BranchAlreadyExists($already); 50 | } 51 | 52 | $objectName = $result->getError()->match('/fatal: Not a valid object name: \'(.*)\'/i'); 53 | if ($objectName->isNotEmpty()) { 54 | throw new ObjectNameNotValid($objectName); 55 | } 56 | } 57 | ])) 58 | ->executeOrFail($this->executor); 59 | } 60 | 61 | public function getAll(): array 62 | { 63 | $result = $this 64 | ->builder 65 | ->make() 66 | ->addArgument('branch') 67 | ->addCutOption('a') 68 | ->executeOrFail($this->executor); 69 | 70 | return $result->getResult()->trim()->replace([ 71 | '* master' => 'master', 72 | ])->lines()->trim()->toStrings(); 73 | } 74 | 75 | public function current(): Str 76 | { 77 | return $this 78 | ->builder 79 | ->make() 80 | ->addArgument('branch') 81 | ->addOption('show-current') 82 | ->executeOrFail($this->executor) 83 | ->getResult() 84 | ->trim(); 85 | } 86 | 87 | public function switch(string $branch): bool 88 | { 89 | return $this 90 | ->builder 91 | ->make() 92 | ->addArgument('switch') 93 | ->addArgument($branch) 94 | ->setExceptionTrigger(UserExceptionTrigger::fromCallbacks([ 95 | function (CommandResult $result) use ($branch) { 96 | if ($result->getError()->contains('fatal: invalid reference: '. $branch)) { 97 | throw new ReferenceInvalid($branch); 98 | } 99 | 100 | if ($result->getError()->contains("Already on '$branch'")) { 101 | throw new AlreadySwitched($branch); 102 | } 103 | } 104 | ])) 105 | ->executeOrFail($this->executor) 106 | ->getResult() 107 | ->contains("Switched to branch '$branch'"); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Command/Commands/CommitCommand.php: -------------------------------------------------------------------------------- 1 | adds = $adds; 32 | $this->statuses = $statuses; 33 | 34 | parent::__construct($builder, $executor); 35 | } 36 | 37 | public function commit(string $message, bool $amend = false, ?Author $author = null): bool 38 | { 39 | return $this 40 | ->builder 41 | ->make() 42 | ->addArgument('commit') 43 | ->when($author !== null, function (ShellCommandInterface $command) use ($author) { 44 | $command->addOptionWithValue('author', $author, true); 45 | }) 46 | ->addCutOption('m') 47 | ->addArgument($message, true) 48 | ->when($amend === true, function (ShellCommandInterface $command) { 49 | $command->addOption('amend'); 50 | }) 51 | ->setExceptionTrigger(UserExceptionTrigger::fromCallbacks([ 52 | function (CommandResult $result) { 53 | if ($result->getError()->contains('nothing to commit')) { 54 | throw new NothingToCommit(); 55 | } 56 | } 57 | ])) 58 | ->executeOrFail($this->executor) 59 | ->getResult() 60 | ->contains('file changed'); 61 | } 62 | 63 | /** 64 | * equals: git add (untracked files) && git commit -m $message 65 | * @codeCoverageIgnore 66 | */ 67 | public function autoCommit(string $message, bool $amend = false): bool 68 | { 69 | $this->adds->add($this->statuses->getModifiedFiles()); 70 | 71 | return $this->commit($message, $amend); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Command/Commands/ConfigCommand.php: -------------------------------------------------------------------------------- 1 | reader = $reader; 25 | 26 | parent::__construct($builder, $executor); 27 | } 28 | 29 | public function getAll(): SubjectsCollection 30 | { 31 | return $this->reader->parse($this->executeConfigList()); 32 | } 33 | 34 | public function getSubject(string $prefix): ConfigSubject 35 | { 36 | return $this->reader->parseByPrefix($this->executeConfigList(), $prefix); 37 | } 38 | 39 | public function set(string $scope, string $field, string $value, bool $replaceAll = false): bool 40 | { 41 | return $this 42 | ->builder 43 | ->make() 44 | ->addArgument('config') 45 | ->addArgument("$scope.$field") 46 | ->addArgument($value, true) 47 | ->when($replaceAll === true, function (ShellCommandInterface $command) { 48 | $command->addOption('replace-all'); 49 | }) 50 | ->executeOrFail($this->executor) 51 | ->isOk(); 52 | } 53 | 54 | protected function executeConfigList(): Str 55 | { 56 | return $this 57 | ->builder 58 | ->make() 59 | ->addArgument('config') 60 | ->addOption('list') 61 | ->executeOrFail($this->executor) 62 | ->getResult(); 63 | } 64 | 65 | public function unset(string $scope, string $field): void 66 | { 67 | $this 68 | ->builder 69 | ->make() 70 | ->addArgument('config') 71 | ->addOption('unset') 72 | ->addArgument("$scope.$field") 73 | ->setExceptionTrigger(UserExceptionTrigger::fromCallbacks([ 74 | function (CommandResult $result) { 75 | $error = $result->getError()->trim(); 76 | 77 | $section = $error->match('#key does not contain a section: (.*)#'); 78 | 79 | if ($section->isNotEmpty()) { 80 | throw new ConfigSectionNotFound($section); 81 | } 82 | 83 | $variable = $error->match('#key does not contain variable name: (.*)#'); 84 | 85 | if ($variable->isNotEmpty()) { 86 | throw new ConfigVariableNotFound($variable); 87 | } 88 | }, 89 | ])) 90 | ->executeOrFail($this->executor); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Command/Commands/FetchCommand.php: -------------------------------------------------------------------------------- 1 | buildFetchCommand()->executeOrFail($this->executor); 13 | } 14 | 15 | public function fetchAll(): void 16 | { 17 | $this->buildFetchCommand()->addOption('all')->executeOrFail($this->executor); 18 | } 19 | 20 | protected function buildFetchCommand(): ShellCommandInterface 21 | { 22 | return $this->builder->make()->addArgument('fetch'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Command/Commands/FileCommand.php: -------------------------------------------------------------------------------- 1 | files = $files; 21 | $this->context = $context; 22 | } 23 | 24 | public function createFile(string $name, string $content, ?string $folder = null): string 25 | { 26 | $path = $this->context->getRootDir(); 27 | 28 | if (! empty($folder)) { 29 | $path .= DIRECTORY_SEPARATOR . $folder; 30 | 31 | $this->createFolder($folder); 32 | } 33 | 34 | $path .= DIRECTORY_SEPARATOR . $name; 35 | 36 | $this->files->createFile($path, $content); 37 | 38 | return $path; 39 | } 40 | 41 | public function createFolder(string $name): GitFileCommand 42 | { 43 | $this->files->createDir($this->createPathTo($name)); 44 | 45 | return $this; 46 | } 47 | 48 | public function getContent(string $name): string 49 | { 50 | return $this->files->getFileContent($this->createPathTo($name)); 51 | } 52 | 53 | public function createPathTo(string $name): string 54 | { 55 | return $this->context->getRootDir() . DIRECTORY_SEPARATOR . $name; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Command/Commands/GarbageCommand.php: -------------------------------------------------------------------------------- 1 | analyzeCollectAnswer($this->buildPureCommand($mode)->executeOrFail($this->executor)); 15 | } 16 | 17 | public function collectOnDate(GarbageCollectMode $mode, \DateTimeInterface $date): bool 18 | { 19 | return $this->analyzeCollectAnswer($this 20 | ->buildPureCommand($mode) 21 | ->addOptionWithValue('prune', $date->format('Y-m-d')) 22 | ->executeOrFail($this->executor)); 23 | } 24 | 25 | protected function analyzeCollectAnswer(CommandResult $result): bool 26 | { 27 | if ($result->getResult()->contains('Nothing new to pack.')) { 28 | return false; 29 | } 30 | 31 | return true; 32 | } 33 | 34 | protected function buildPureCommand(GarbageCollectMode $mode): ShellCommandInterface 35 | { 36 | return $this->builder->make()->addArgument('gc')->addOption($mode->value); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Command/Commands/GrepCommand.php: -------------------------------------------------------------------------------- 1 | .*)\:(?\d+)\:(?.*)\n?/i'; 11 | 12 | public function grep(string $term): array 13 | { 14 | $result = $this->builder->make() 15 | ->addOption('no-pager') 16 | ->addArgument('grep') 17 | ->addCutOption('n') 18 | ->addArgument($term, true) 19 | ->executeOrFail($this->executor) 20 | ->getResult(); 21 | 22 | if ($result->isEmpty()) { 23 | return []; 24 | } 25 | 26 | $matches = []; 27 | 28 | foreach ($result->globalMatch($this->grepRegex) as $match) { 29 | $matches[] = FileMatch::fromArray($match); 30 | } 31 | 32 | return $matches; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Command/Commands/HelpCommand.php: -------------------------------------------------------------------------------- 1 | builder->make()->addOption('help')->executeOrFail($this->executor); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Command/Commands/HookCommand.php: -------------------------------------------------------------------------------- 1 | fileSystem = $fileSystem; 31 | $this->executor = $executor; 32 | $this->context = $context; 33 | } 34 | 35 | /** 36 | * @see HookName for $name 37 | */ 38 | public function add(HookName $name, string $script): bool 39 | { 40 | $path = $this->doAdd($name, $script); 41 | 42 | Chmod::executable($path)->executeOrFail($this->executor); 43 | 44 | return $this->has($name); 45 | } 46 | 47 | public function get(HookName $name): Hook 48 | { 49 | if (! $this->has($name)) { 50 | throw new HookNotExists($name); 51 | } 52 | 53 | return $this->makeHookObject($name); 54 | } 55 | 56 | /** 57 | * @see HookName for $name 58 | */ 59 | public function has(HookName $name): bool 60 | { 61 | return $this->fileSystem->exists($this->getHookPath($name)); 62 | } 63 | 64 | public function delete(HookName $name): bool 65 | { 66 | if (! $this->has($name)) { 67 | throw new HookNotExists($name); 68 | } 69 | 70 | return $this->fileSystem->removeFile($this->getHookPath($name)); 71 | } 72 | 73 | /** 74 | * @return array 75 | */ 76 | public function getAll(bool $onlyWorked = true): array 77 | { 78 | $map = []; 79 | $paths = $this->fileSystem->getFromDirectory($this->getHookPath()); 80 | 81 | if ($onlyWorked) { 82 | $paths = array_filter($paths, function (string $path) { 83 | return empty(pathinfo($path, PATHINFO_EXTENSION)); 84 | }); 85 | } 86 | 87 | $existsNames = array_map(function (string $path) { 88 | return pathinfo($path, PATHINFO_BASENAME); 89 | }, $paths); 90 | 91 | foreach ($existsNames as $name) { 92 | $map[$name] = $this->makeHookObject($name); 93 | } 94 | 95 | return $map; 96 | } 97 | 98 | /** 99 | * @codeCoverageIgnore 100 | */ 101 | public function getHookPath(?string $name = null): string 102 | { 103 | return $this->context->getGitDir() . DIRECTORY_SEPARATOR . 'hooks'. DIRECTORY_SEPARATOR . $name; 104 | } 105 | 106 | protected function makeHookObject(string $name): Hook 107 | { 108 | return new Hook( 109 | $name, 110 | $this->fileSystem->getFileContent($path = $this->getHookPath($name)), 111 | $this->fileSystem->getLastUpdateDate($path) 112 | ); 113 | } 114 | 115 | protected function doAdd(string $name, string $script): string 116 | { 117 | $this 118 | ->fileSystem 119 | ->createFile($path = $this->getHookPath($name), $script); 120 | 121 | return $path; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Command/Commands/IgnoreCommand.php: -------------------------------------------------------------------------------- 1 | context = $context; 25 | $this->fileSystem = $fileSystem; 26 | $this->seeToRoot(); 27 | } 28 | 29 | /** 30 | * @return array 31 | */ 32 | public function files(): array 33 | { 34 | $path = $this->getPath(); 35 | 36 | if (! $this->fileSystem->exists($path)) { 37 | return []; 38 | } 39 | 40 | return Str::make($this->fileSystem->getFileContent($this->getPath()))->lines()->trim()->toStrings(); 41 | } 42 | 43 | public function add(string $path): bool 44 | { 45 | $gitIgnore = $this->getPath(); 46 | 47 | $content = $this->fileSystem->exists($gitIgnore) ? $this->fileSystem->getFileContent($gitIgnore) : ''; 48 | $content = new Str($content); 49 | 50 | if (! $content->isEmpty()) { 51 | $content = $content->append("\n"); 52 | } 53 | 54 | return $this->fileSystem->createFile($gitIgnore, $content->append($path)); 55 | } 56 | 57 | public function delete(string $path): bool 58 | { 59 | if (! $this->fileSystem->exists($gitignore = $this->getPath())) { 60 | return false; 61 | } 62 | 63 | $content = Str::make($this->fileSystem->getFileContent($gitignore))->lines(); 64 | $excepts = []; 65 | 66 | foreach ($content as $index => $line) { 67 | if ($line->trim()->equals($path)) { 68 | $excepts[] = $index; 69 | } 70 | } 71 | 72 | return $this->fileSystem->createFile($gitignore, $content->exceptKeys($excepts)->implodeAsLines()); 73 | } 74 | 75 | public function has(string $path): bool 76 | { 77 | if (! $this->isFileExists()) { 78 | return false; 79 | } 80 | 81 | return (new Str($this->fileSystem->getFileContent($this->getPath()))) 82 | ->hasLine($path); 83 | } 84 | 85 | /** 86 | * @codeCoverageIgnore 87 | */ 88 | public function getPath(): string 89 | { 90 | return $this->folder . DIRECTORY_SEPARATOR . '.gitignore'; 91 | } 92 | 93 | final protected function isFileExists(): bool 94 | { 95 | return $this->fileSystem->exists($this->getPath()); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Command/Commands/LogCommand.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 24 | 25 | parent::__construct($builder, $executor); 26 | } 27 | 28 | public function getAll(): ?LogCollection 29 | { 30 | return $this->executeAndParseLogCommand($this->buildLogCommand()); 31 | } 32 | 33 | public function get(callable $callback): ?LogCollection 34 | { 35 | $callback($builder = new LogBuilder()); 36 | 37 | return $this->executeAndParseLogCommand($builder->build($this->buildLogCommand())); 38 | } 39 | 40 | public function count(callable $callback): int 41 | { 42 | $callback($builder = new LogBuilder()); 43 | 44 | return $builder 45 | ->build($this->buildLogCommand()) 46 | ->addPipe() 47 | ->addArgument('wc') 48 | ->addCutOption('l') 49 | ->executeOrFail($this->executor) 50 | ->getResult() 51 | ->toInteger(); 52 | } 53 | 54 | protected function executeAndParseLogCommand(ShellCommandInterface $command): ?LogCollection 55 | { 56 | return $this->parser->parse($command->executeOrFail($this->executor)->getResult()); 57 | } 58 | 59 | protected function buildLogCommand(): ShellCommandInterface 60 | { 61 | return $this->builder->make() 62 | ->addArgument('log') 63 | ->addOption('oneline') 64 | ->addOption('decorate') 65 | ->addOption('graph') 66 | ->addOptionWithValue('pretty', "format:'|log-entry|%H|%ad|%an|%ae|%Creset%s|'") 67 | ->addOptionWithValue('date', 'iso') 68 | ->setExceptionTrigger(UserExceptionTrigger::fromCallbacks([ 69 | function (CommandResult $result) { 70 | $branch = $result 71 | ->getError() 72 | ->match("/fatal: your current branch '(.*)' does not have any commits yet/i"); 73 | 74 | if ($branch->isNotEmpty()) { 75 | throw new BranchDoesNotHaveCommits($branch); 76 | } 77 | } 78 | ])); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Command/Commands/MergeCommand.php: -------------------------------------------------------------------------------- 1 | buildMergeCommand($branch, $message)->executeOrFail($this->executor); 17 | } 18 | 19 | public function mergeSquash(string $branch, ?string $message = null): void 20 | { 21 | $this->buildMergeCommand($branch, $message)->addOption('squash')->executeOrFail($this->executor); 22 | } 23 | 24 | public function abort(): void 25 | { 26 | $this 27 | ->builder 28 | ->make() 29 | ->addArgument('merge') 30 | ->addOption('abort') 31 | ->setExceptionTrigger(UserExceptionTrigger::fromCallbacks([ 32 | function (CommandResult $result) { 33 | if ($result 34 | ->getError() 35 | ->contains('fatal: There is no merge to abort (MERGE_HEAD missing)')) { 36 | throw new MergeHeadMissing(); 37 | } 38 | } 39 | ])) 40 | ->executeOrFail($this->executor); 41 | } 42 | 43 | protected function buildMergeCommand(string $branch, ?string $message = null): ShellCommandInterface 44 | { 45 | return $this 46 | ->builder 47 | ->make() 48 | ->addArgument('merge') 49 | ->addArgument($branch) 50 | ->setExceptionTrigger(UserExceptionTrigger::fromCallbacks([ 51 | function (CommandResult $result) { 52 | $failedBranch = $result->getError()->match('#merge: (.*) - not something we can merge#i'); 53 | if ($failedBranch->isNotEmpty()) { 54 | throw new NotSomethingWeCanMerge($failedBranch); 55 | } 56 | } 57 | ])) 58 | ->when($message !== null, function (ShellCommandInterface $command) use ($message) { 59 | $command->addCutOption('m')->addArgument($message); 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Command/Commands/PathCommand.php: -------------------------------------------------------------------------------- 1 | getPathByOption('info-path'); 12 | } 13 | 14 | public function html(): string 15 | { 16 | return $this->getPathByOption('html-path'); 17 | } 18 | 19 | public function man(): string 20 | { 21 | return $this->getPathByOption('man-path'); 22 | } 23 | 24 | protected function getPathByOption(string $option): string 25 | { 26 | return $this->builder->make()->addOption($option)->executeOrFail($this->executor)->getResult()->trim(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Command/Commands/PullCommand.php: -------------------------------------------------------------------------------- 1 | */ 11 | protected static $okResults = [ 12 | 'Already up to date', 13 | 'Receiving objects', 14 | 'Resolving deltas', 15 | ]; 16 | 17 | public function pull(): bool 18 | { 19 | return $this 20 | ->buildPurePullCommand() 21 | ->executeOrFail($this->executor) 22 | ->getResult() 23 | ->containsAny(static::$okResults); 24 | } 25 | 26 | public function pullBranch(string $branch): bool 27 | { 28 | return $this 29 | ->buildPurePullCommand() 30 | ->addArgument($branch) 31 | ->executeOrFail($this->executor) 32 | ->getResult() 33 | ->containsAny(static::$okResults); 34 | } 35 | 36 | protected function buildPurePullCommand(): ShellCommandInterface 37 | { 38 | return $this->builder->make()->addArgument('pull'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Command/Commands/RemoteCommand.php: -------------------------------------------------------------------------------- 1 | executeShowRemote(); 20 | 21 | return new Remotes( 22 | $sh->match('/origin\s+(.*)\(fetch\)/')->trim(), 23 | $sh->match('/origin\s+(.*)\(push\)/')->trim() 24 | ); 25 | } 26 | 27 | public function hasAnyRemoteUrl(string $url): bool 28 | { 29 | $remotes = $this->show(); 30 | 31 | return $remotes->fetch->equals($url) || $remotes->push->equals($url); 32 | } 33 | 34 | /** 35 | * @inheritDoc 36 | */ 37 | public function remove(string $shortName): bool 38 | { 39 | return $this 40 | ->builder 41 | ->make() 42 | ->addArgument('remote') 43 | ->addArgument('remove') 44 | ->addArgument($shortName) 45 | ->setExceptionTrigger(UserExceptionTrigger::fromCallbacks([ 46 | function (CommandResult $result) { 47 | $notFound = $result->getError()->match("/No such remote: '(.*)'/i"); 48 | if ($notFound->isNotEmpty()) { 49 | throw new RemoteNotFound($notFound); 50 | } 51 | } 52 | ])) 53 | ->executeOrFail($this->executor) 54 | ->isOk(); 55 | } 56 | 57 | /** 58 | * @inheritDoc 59 | */ 60 | public function add(string $shortName, string $url): bool 61 | { 62 | return $this 63 | ->builder 64 | ->make() 65 | ->addArgument('remote') 66 | ->addArgument('add') 67 | ->addArgument($shortName) 68 | ->addArgument($url) 69 | ->setExceptionTrigger(UserExceptionTrigger::fromCallbacks([ 70 | function (CommandResult $result) { 71 | $already = $result->getError()->match('/remote (.*) already exists/i'); 72 | if ($already->isNotEmpty()) { 73 | throw new RemoteAlreadyExists($already); 74 | } 75 | } 76 | ])) 77 | ->executeOrFail($this->executor) 78 | ->isOk(); 79 | } 80 | 81 | /** 82 | * equals: git remote -v 83 | */ 84 | protected function executeShowRemote(): Str 85 | { 86 | return $this 87 | ->builder 88 | ->make() 89 | ->addArgument('remote') 90 | ->addOption('v') 91 | ->setExceptionTrigger(UserExceptionTrigger::fromCallbacks([ 92 | function (CommandResult $result) { 93 | $failed = $result->getError()->match("/repository '(.*)' not found/i"); 94 | if ($failed->isNotEmpty()) { 95 | throw new RemoteRepositoryNotFound($failed); 96 | } 97 | } 98 | ])) 99 | ->executeOrFail($this->executor) 100 | ->getResult(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Command/Commands/SetupCommand.php: -------------------------------------------------------------------------------- 1 | remotes = $remotes; 37 | $this->files = $fileSystem; 38 | $this->context = $context; 39 | 40 | parent::__construct($builder, $executor); 41 | } 42 | 43 | public function init(): bool 44 | { 45 | if ($this->isInit()) { 46 | throw new RepositoryAlreadyExists($this->context->getGitDir()); 47 | } elseif (! $this->files->exists($this->context->getGitDir())) { 48 | $this->files->createDir($this->context->getRootDir()); 49 | } 50 | 51 | return $this 52 | ->builder 53 | ->make() 54 | ->addArgument('init') 55 | ->executeOrFail($this->executor) 56 | ->getResult() 57 | ->contains('Initialized empty Git repository'); 58 | } 59 | 60 | public function isInit(): bool 61 | { 62 | return $this->files->exists($this->context->getGitDir()); 63 | } 64 | 65 | public function clone(string $url, ?string $branch = null, ?string $folder = null): bool 66 | { 67 | return $this 68 | ->builder 69 | ->make($this->files->downPath($this->context->getRootDir())) 70 | ->addArgument('clone') 71 | ->when($branch !== null, function (ShellCommand $command) use ($branch) { 72 | $command 73 | ->addCutOption('b') 74 | ->addArgument($branch); 75 | }) 76 | ->addArgument($url) 77 | ->addArgument($folder = $folder ?? $this->context->getRootFolder()) 78 | ->setExceptionTrigger(UserExceptionTrigger::fromCallbacks([ 79 | function (CommandResult $result) use ($folder) { 80 | PathAlreadyExists::handleIfSo($folder, $result->getError()); 81 | } 82 | ])) 83 | ->executeOrFail($this->executor) 84 | ->getResult() 85 | ->contains("Cloning into '{$folder}'"); 86 | } 87 | 88 | /** 89 | * Delete this repository 90 | */ 91 | public function delete(): bool 92 | { 93 | return $this->files->removeDir($this->context->getRootDir()); 94 | } 95 | 96 | /** 97 | * Delete local repository and fetch from origin 98 | */ 99 | public function reinstall(?string $branch = null): void 100 | { 101 | $remote = $this->remotes->show()->fetch; 102 | 103 | if ($remote->isEmpty()) { 104 | throw new RemoteNotFilled(); 105 | } 106 | 107 | $this->delete(); 108 | 109 | $this->clone($remote, $branch); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Command/Commands/StashCommand.php: -------------------------------------------------------------------------------- 1 | builder 20 | ->make() 21 | ->addArgument('stash') 22 | ->when($message !== null, function (ShellCommand $command) use ($message) { 23 | $command 24 | ->addArgument('save') 25 | ->addArgument('"'. $message .'"'); 26 | }) 27 | ->executeOrFail($this->executor) 28 | ->getResult() 29 | ->containsAny([ 30 | 'Saved working directory and index', 31 | 'No local changes to save', 32 | ]); 33 | } 34 | 35 | public function pop(): bool 36 | { 37 | return $this 38 | ->builder 39 | ->make() 40 | ->addArgument('stash') 41 | ->addArgument('pop') 42 | ->executeOrFail($this->executor) 43 | ->getResult() 44 | ->contains('Changes not staged for commit:'); 45 | } 46 | 47 | /** 48 | * @return array 49 | */ 50 | public function getList(): array 51 | { 52 | $result = $this 53 | ->builder 54 | ->make() 55 | ->addOption('no-pager') 56 | ->addArgument('stash') 57 | ->addArgument('list') 58 | ->addOptionWithValue('pretty', FormatPlaceholder::format([ 59 | FormatPlaceholder::REFLOG_SHORTENED_SELECTOR, 60 | FormatPlaceholder::REFLOG_SUBJECT, 61 | ])) 62 | ->executeOrFail($this->executor) 63 | ->getResult(); 64 | 65 | return array_map(function (array $data) { 66 | return new Stash($data[1], $data[2], trim($data[3])); 67 | }, $result->globalMatch('/stash@{(.*)}\|.*on (.*):(.*)/i')); 68 | } 69 | 70 | public function apply(int $id): bool 71 | { 72 | return $this 73 | ->builder 74 | ->make() 75 | ->addArgument('stash') 76 | ->addArgument('apply') 77 | ->addArgument('stash@{'. $id . '}') 78 | ->setExceptionTrigger(UserExceptionTrigger::fromCallbacks([ 79 | function (CommandResult $result) use ($id) { 80 | if ($result->getError()->contains("fatal: Log for 'stash' only has (.*) entries", true)) { 81 | throw new StashDoesNotExists($id); 82 | } 83 | } 84 | ])) 85 | ->executeOrFail($this->executor) 86 | ->getResult() 87 | ->containsAny(['Changes not staged for commit', 'Changes to be committed']); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Command/Commands/StatusCommand.php: -------------------------------------------------------------------------------- 1 | builder 16 | ->make() 17 | ->addArgument('status') 18 | ->when($short, function (ShellCommand $command) { 19 | $command->addCutOption('s'); 20 | }) 21 | ->executeOrFail($this->executor) 22 | ->getResult() 23 | ->trim(); 24 | } 25 | 26 | public function hasChanges(): bool 27 | { 28 | $status = $this->status(true); 29 | $groups = $this->getGroupsByStatusResult($status); 30 | 31 | return $status->isNotEmpty() && ( 32 | array_key_exists(StatusResult::GROUP_MODIFIED, $groups) || 33 | array_key_exists(StatusResult::GROUP_ADDED, $groups)); 34 | } 35 | 36 | public function getUntrackedFiles(): array 37 | { 38 | return $this 39 | ->getGroupsByStatusResult($this->status(true)->trim())[StatusResult::GROUP_UNTRACKED] ?? []; 40 | } 41 | 42 | public function getModifiedFiles(): array 43 | { 44 | return $this->getGroupsByStatusResult($this->status(true)->trim())[StatusResult::GROUP_MODIFIED] ?? []; 45 | } 46 | 47 | public function getAddedFiles(): array 48 | { 49 | return $this->getGroupsByStatusResult($this->status(true)->trim())[StatusResult::GROUP_ADDED] ?? []; 50 | } 51 | 52 | /** 53 | * @return array> 54 | */ 55 | protected function getGroupsByStatusResult(Str $result): array 56 | { 57 | $groups = []; 58 | 59 | if ($result->isEmpty()) { 60 | return []; 61 | } 62 | 63 | foreach ($result->lines() as $line) { 64 | [$group, $file] = $line->trim()->explode(' '); 65 | 66 | if ($group === null || $file === null) { 67 | continue; 68 | } 69 | 70 | $groups[$group->__toString()][] = $file->__toString(); 71 | } 72 | 73 | return $groups; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Command/Commands/TagCommand.php: -------------------------------------------------------------------------------- 1 | builder->make()->addArgument('tag') 22 | ->when($pattern !== null, function (ShellCommand $command) use ($pattern) { 23 | $command 24 | ->addCutOption('l') 25 | ->addArgument($pattern, true); 26 | })->executeOrFail($this->executor)->getResult()->trim(); 27 | 28 | return $result->isEmpty() ? [] : $result->lines()->toStrings(); 29 | } 30 | 31 | public function add(string $tag, ?string $message = null): bool 32 | { 33 | if ($this->exists($tag)) { 34 | throw new TagAlreadyExists($tag); 35 | } 36 | 37 | return $this 38 | ->builder 39 | ->make() 40 | ->addArgument('tag') 41 | ->addCutOption('a') 42 | ->addArgument($tag) 43 | ->addCutOption('m') 44 | ->addArgument($message ?? "Version {$tag}", true) 45 | ->executeOrFail($this->executor) 46 | ->isEmpty(); 47 | } 48 | 49 | public function get(string $tagName): Tag 50 | { 51 | $result = $this 52 | ->builder 53 | ->make() 54 | ->addArgument('show') 55 | ->addArgument($tagName) 56 | ->addOptionWithValue('pretty', FormatPlaceholder::format([ 57 | FormatPlaceholder::AUTHOR_NAME, 58 | FormatPlaceholder::AUTHOR_EMAIL, 59 | FormatPlaceholder::AUTHOR_DATE_RFC2822, 60 | FormatPlaceholder::COMMIT_HASH, 61 | FormatPlaceholder::SUBJECT, 62 | ])) 63 | ->addCutOption('s') 64 | ->setExceptionTrigger(UserExceptionTrigger::fromCallbacks([ 65 | function (CommandResult $result) use ($tagName) { 66 | if ($result 67 | ->getError() 68 | ->contains( 69 | "ambiguous argument '$tagName': unknown revision or path not in the working tree" 70 | )) { 71 | throw new TagNotFound($tagName); 72 | } 73 | } 74 | ])) 75 | ->executeOrFail($this->executor); 76 | 77 | $parts = $result->getResult()->explode('|'); 78 | 79 | if ($parts->count() !== 5) { 80 | throw new UnexpectedException($result->getCommandLine()); 81 | } 82 | 83 | [$authorName, $authorEmail, $date, $commit, $message] = $parts->toStrings(); 84 | 85 | return new Tag( 86 | new Author($authorName, $authorEmail), 87 | new \DateTime($date), 88 | new Commit($commit), 89 | $message 90 | ); 91 | } 92 | 93 | public function exists(string $tag): bool 94 | { 95 | return in_array($tag, $this->getAll()); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Command/GitCommandBuilder.php: -------------------------------------------------------------------------------- 1 | builder = $builder; 22 | $this->bin = $bin; 23 | $this->dir = $dir; 24 | } 25 | 26 | public function make(?string $dir = null): ShellCommandInterface 27 | { 28 | return $this->builder->makeNavigateToDir($dir ?? $this->dir, $this->bin); 29 | } 30 | 31 | /** 32 | * @codeCoverageIgnore 33 | */ 34 | public function toDir(string $dir, string $bin): ShellCommandInterface 35 | { 36 | return $this->builder->makeNavigateToDir($dir, $bin); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Concerns/SwitchFolder.php: -------------------------------------------------------------------------------- 1 | folder = $this->context->getRootDir(); 12 | 13 | return $this; 14 | } 15 | 16 | public function seeToFolder(string $folder): self 17 | { 18 | $this->folder = $this->context->getRootDir() . DIRECTORY_SEPARATOR . $folder; 19 | 20 | return $this; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Config/Configurators/AliasConfigurator.php: -------------------------------------------------------------------------------- 1 | $script) { 17 | $aliases[$name] = new Alias($name, $script); 18 | } 19 | 20 | return new AliasList($aliases); 21 | } 22 | 23 | /** 24 | * @codeCoverageIgnore 25 | */ 26 | public function getPrefix(): string 27 | { 28 | return 'alias'; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Config/Configurators/BranchConfigurator.php: -------------------------------------------------------------------------------- 1 | $url) { 23 | $nameParts = explode('.', $identity); 24 | $branchName = strlen($branchName) ? $branchName : $this->buildBranchName($nameParts); 25 | $links[end($nameParts)] = $url; 26 | } 27 | 28 | $branches[$branchName] = Branch::fromLinks($branchName, $links); 29 | } 30 | 31 | return new BranchList($branches); 32 | } 33 | 34 | /** 35 | * @codeCoverageIgnore 36 | */ 37 | public function getPrefix(): string 38 | { 39 | return 'branch'; 40 | } 41 | 42 | /** 43 | * @param array $parts 44 | */ 45 | protected function buildBranchName(array $parts): string 46 | { 47 | return Str::fromArray(array_slice($parts, 0, -1), '.') 48 | ->cut(null, strlen($this->getPrefix()) + 1); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Config/Configurators/CommitConfigurator.php: -------------------------------------------------------------------------------- 1 | $raw 13 | * @return User 14 | */ 15 | public function parse(array $raw): ConfigSubject 16 | { 17 | return new User($raw['name'] ?? '', $raw['email'] ?? ''); 18 | } 19 | 20 | /** 21 | * @inheritDoc 22 | * @codeCoverageIgnore 23 | */ 24 | public function getPrefix(): string 25 | { 26 | return 'user'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Config/ConfiguratorsDict.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class ConfiguratorsDict implements \IteratorAggregate 12 | { 13 | protected $configurators; 14 | 15 | /** 16 | * @param array $configurators 17 | * @codeCoverageIgnore 18 | */ 19 | public function __construct(array $configurators) 20 | { 21 | $this->configurators = $configurators; 22 | } 23 | 24 | /** 25 | * @param list $configurators 26 | */ 27 | public static function make(iterable $configurators): self 28 | { 29 | $dict = []; 30 | 31 | foreach ($configurators as $configurator) { 32 | $dict[$configurator->getPrefix()] = $configurator; 33 | } 34 | 35 | return new self($dict); 36 | } 37 | 38 | /** 39 | * @throws SubjectConfiguratorNotFound 40 | */ 41 | public function getOrFail(string $prefix): SubjectConfigurator 42 | { 43 | if (! $this->has($prefix)) { 44 | throw new SubjectConfiguratorNotFound($prefix); 45 | } 46 | 47 | return $this->configurators[$prefix]; 48 | } 49 | 50 | public function has(string $prefix): bool 51 | { 52 | return array_key_exists($prefix, $this->configurators); 53 | } 54 | 55 | /** 56 | * @return iterable 57 | */ 58 | public function getIterator(): iterable 59 | { 60 | return new \ArrayIterator($this->configurators); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Config/RegexConfigResultParser.php: -------------------------------------------------------------------------------- 1 | configurators = $configurators; 26 | } 27 | 28 | /** 29 | * @inheritDoc 30 | */ 31 | public function parse(Str $raw): SubjectsCollection 32 | { 33 | $grouped = $this->grouped($raw->globalMatch($this->regex), 1); 34 | 35 | // 36 | 37 | $data = []; 38 | 39 | foreach ($grouped as $scope => $item) { 40 | if (! $this->configurators->has($scope)) { 41 | continue; 42 | } 43 | 44 | $data[] = $this->configurators->getOrFail($scope)->parse($item); 45 | } 46 | 47 | return new SubjectsCollection($data); 48 | } 49 | 50 | /** 51 | * @inheritDoc 52 | */ 53 | public function parseByPrefix(Str $raw, string $prefix): ConfigSubject 54 | { 55 | $grouped = $this->grouped($raw->globalMatch($this->regex), 1); 56 | 57 | // 58 | 59 | if (! array_key_exists($prefix, $grouped)) { 60 | throw new ConfigDataNotFound($prefix); 61 | } 62 | 63 | return $this->configurators->getOrFail($prefix)->parse($grouped[$prefix]); 64 | } 65 | 66 | /** 67 | * @param array> $matches 68 | * @return array> 69 | */ 70 | protected function grouped(array $matches, int $matchOffset): array 71 | { 72 | $grouped = []; 73 | 74 | foreach ($matches as $match) { 75 | [$scope, $field, $value] = array_slice($match, $matchOffset); 76 | 77 | $grouped[$scope][$field] = $value; 78 | } 79 | 80 | return $grouped; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Config/Subjects/AbstractSubject.php: -------------------------------------------------------------------------------- 1 | name = $name; 19 | $this->script = $script; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Config/Subjects/AliasList.php: -------------------------------------------------------------------------------- 1 | $aliases 13 | */ 14 | public function __construct(array $aliases) 15 | { 16 | $this->aliases = $aliases; 17 | } 18 | 19 | /** 20 | * @return iterable 21 | */ 22 | public function getIterator(): iterable 23 | { 24 | return new \ArrayIterator($this->aliases); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Config/Subjects/Branch.php: -------------------------------------------------------------------------------- 1 | name = $name; 26 | $this->remote = $remote; 27 | $this->merge = $merge; 28 | } 29 | 30 | /** 31 | * @param array $links 32 | */ 33 | public static function fromLinks( 34 | string $name, 35 | #[ArrayShape(['remote' => 'string', 'merge' => 'string'])] 36 | array $links 37 | ): self { 38 | return new self($name, $links['remote'] ?? '', $links['merge'] ?? ''); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Config/Subjects/BranchList.php: -------------------------------------------------------------------------------- 1 | $branches 13 | * @codeCoverageIgnore 14 | */ 15 | public function __construct(array $branches) 16 | { 17 | $this->branches = $branches; 18 | } 19 | 20 | public function get(string $name): ?Branch 21 | { 22 | return $this->branches[$name] ?? null; 23 | } 24 | 25 | /** 26 | * @return iterable 27 | */ 28 | public function getIterator(): iterable 29 | { 30 | return new \ArrayIterator($this->branches); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Config/Subjects/ConfigCommit.php: -------------------------------------------------------------------------------- 1 | templatePath = $templatePath; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Config/Subjects/ConfigDiff.php: -------------------------------------------------------------------------------- 1 | externalPath = $externalPath; 18 | $this->renames = $renames; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Config/Subjects/ConfigSubmodule.php: -------------------------------------------------------------------------------- 1 | name = $name; 22 | $this->url = $url; 23 | $this->active = $active; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Config/Subjects/ConfigSubmoduleList.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ConfigSubmoduleList extends AbstractSubject implements \IteratorAggregate 11 | { 12 | public $submodules; 13 | 14 | /** 15 | * @param array $submodules 16 | */ 17 | public function __construct(array $submodules) 18 | { 19 | $this->submodules = $submodules; 20 | } 21 | 22 | public function get(string $name): ?ConfigSubmodule 23 | { 24 | return $this->submodules[$name] ?? null; 25 | } 26 | 27 | /** 28 | * @return iterable 29 | */ 30 | public function getIterator(): iterable 31 | { 32 | return new \ArrayIterator($this->submodules); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Config/Subjects/Core.php: -------------------------------------------------------------------------------- 1 | autocrlf = $autocrlf; 45 | $this->ignoreCase = $ignoreCase; 46 | $this->repositoryFormatVersion = $repositoryFormatVersion; 47 | $this->bare = $bare; 48 | $this->logAllRefUpdates = $logAllRefUpdates; 49 | $this->preComposeUnicode = $preComposeUnicode; 50 | $this->fileMode = $fileMode; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Config/Subjects/Credential.php: -------------------------------------------------------------------------------- 1 | helper = $helper; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Config/Subjects/Instaweb.php: -------------------------------------------------------------------------------- 1 | local = $local; 25 | $this->httpd = $httpd; 26 | $this->port = $port; 27 | $this->browser = $browser; 28 | $this->modulePath = $modulePath; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Config/Subjects/Pack.php: -------------------------------------------------------------------------------- 1 | windowMemory = $windowMemory; 41 | $this->packSizeLimit = $packSizeLimit; 42 | $this->threads = $threads; 43 | $this->deltaCacheSize = $deltaCacheSize; 44 | $this->sizeLimit = $sizeLimit; 45 | $this->window = $window; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Config/Subjects/SubjectsCollection.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class SubjectsCollection implements \IteratorAggregate, \Countable 11 | { 12 | protected $subjects; 13 | 14 | /** 15 | * @param array $subjects 16 | * @codeCoverageIgnore 17 | */ 18 | public function __construct(array $subjects) 19 | { 20 | $this->subjects = $subjects; 21 | } 22 | 23 | public function count(): int 24 | { 25 | return count($this->subjects); 26 | } 27 | 28 | /** 29 | * @return iterable 30 | */ 31 | public function getIterator(): iterable 32 | { 33 | return new \ArrayIterator($this->subjects); 34 | } 35 | 36 | /** 37 | * @return ConfigSubject[] 38 | */ 39 | public function all(): array 40 | { 41 | return $this->subjects; 42 | } 43 | 44 | /** 45 | * @return array> 46 | */ 47 | public function toArray(): array 48 | { 49 | $array = []; 50 | 51 | foreach ($this->subjects as $subject) { 52 | $array[$subject->name()] = $subject->toArray(); 53 | } 54 | 55 | return $array; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Config/Subjects/User.php: -------------------------------------------------------------------------------- 1 | name = $name; 23 | $this->email = $email; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Contracts/Attribute/GitAttribute.php: -------------------------------------------------------------------------------- 1 | $raw 13 | */ 14 | public function hydrate( 15 | #[ArrayShape(['name' => 'string', 'email' => 'string'])] 16 | array $raw 17 | ): Author; 18 | } 19 | -------------------------------------------------------------------------------- /src/Contracts/Backup/BackupElement.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | public function dump(GitHandler $git): array; 14 | 15 | /** 16 | * Restore dumped data from array 17 | * @param array $data 18 | */ 19 | public function restore(GitHandler $git, array $data): void; 20 | 21 | /** 22 | * Get this backup element identifier 23 | */ 24 | public function identity(): string; 25 | } 26 | -------------------------------------------------------------------------------- /src/Contracts/Backup/BackupElementDict.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | interface BackupElementDict extends \IteratorAggregate 9 | { 10 | /** 11 | * @param array|string> $classes 12 | * @return array 13 | */ 14 | public function get(array $classes): array; 15 | 16 | /** 17 | * @param array> $classes 18 | * @return static 19 | */ 20 | public function only(array $classes); 21 | 22 | /** 23 | * @return iterable 24 | */ 25 | public function getIterator(); 26 | } 27 | -------------------------------------------------------------------------------- /src/Contracts/Backup/GitBackup.php: -------------------------------------------------------------------------------- 1 | |string> $elements 18 | */ 19 | public function dumpOnly(string $path, array $elements): void; 20 | 21 | /** 22 | * Restore backup 23 | */ 24 | public function restore(string $path): void; 25 | 26 | /** 27 | * Restore backup 28 | * @param non-empty-list|string> $elements 29 | */ 30 | public function restoreOnly(string $path, array $elements): void; 31 | } 32 | -------------------------------------------------------------------------------- /src/Contracts/Commands/FolderSwitchable.php: -------------------------------------------------------------------------------- 1 | $attributes 15 | */ 16 | public function add(string $pattern, array $attributes): void; 17 | 18 | /** 19 | * Find git attribute by pattern 20 | */ 21 | public function find(string $pattern): ?GitAttributes; 22 | 23 | /** 24 | * Delete git attribute by pattern 25 | */ 26 | public function delete(string $pattern): bool; 27 | } 28 | -------------------------------------------------------------------------------- /src/Contracts/Commands/GitBranchCommand.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | public function getAll(): array; 34 | 35 | /** 36 | * Switch to Branch 37 | * @git-command git switch {$branch} 38 | * @throws ReferenceInvalid 39 | * @throws AlreadySwitched 40 | */ 41 | public function switch(string $branch): bool; 42 | 43 | /** 44 | * Get current Branch 45 | * @git-command git branch --show-current 46 | */ 47 | public function current(): Str; 48 | } 49 | -------------------------------------------------------------------------------- /src/Contracts/Commands/GitCommitCommand.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function grep(string $term): array; 18 | } 19 | -------------------------------------------------------------------------------- /src/Contracts/Commands/GitHelpCommand.php: -------------------------------------------------------------------------------- 1 | 38 | */ 39 | public function getAll(bool $onlyWorked = true): array; 40 | 41 | /** 42 | * Get path to hooks storage 43 | */ 44 | public function getHookPath(?string $name = null): string; 45 | } 46 | -------------------------------------------------------------------------------- /src/Contracts/Commands/GitIgnoreCommand.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function files(): array; 15 | 16 | /** 17 | * Add file to .gitignore 18 | */ 19 | public function add(string $path): bool; 20 | 21 | /** 22 | * Delete path from .gitignore 23 | */ 24 | public function delete(string $path): bool; 25 | 26 | /** 27 | * Has file in .gitignore 28 | */ 29 | public function has(string $path): bool; 30 | 31 | /** 32 | * Get full path to file ".gitignore" 33 | */ 34 | public function getPath(): string; 35 | } 36 | -------------------------------------------------------------------------------- /src/Contracts/Commands/GitIndexCommand.php: -------------------------------------------------------------------------------- 1 | $file - file name to git added 20 | */ 21 | public function add($file, bool $force = false): bool; 22 | 23 | /** 24 | * Remove file/files from git index 25 | * @git-command git rm $file 26 | * @git-command git rm $file1 $file2 ... 27 | * @param string|array $files 28 | */ 29 | public function remove($files, bool $force = false): void; 30 | 31 | /** 32 | * Remove cached file/files from git index 33 | * @git-command git rm --cached $file 34 | * @git-command git rm --cached $file1 $file2 ... 35 | * @param string|array $files 36 | */ 37 | public function removeCached($files, bool $force = false): void; 38 | 39 | /** 40 | * Git Reset 41 | * @git-command git reset --$mode $subject 42 | */ 43 | public function reset(ResetMode $mode, string $subject): void; 44 | 45 | /** 46 | * Git Reset Head 47 | * @git-command git reset --$mode HEAD~ 48 | */ 49 | public function resetHead(ResetMode $mode): void; 50 | 51 | /** 52 | * Rollback files state 53 | * @git-command git checkout HEAD $paths 54 | * @param string|array $paths 55 | */ 56 | public function rollback($paths): void; 57 | 58 | /** 59 | * Checkout to paths 60 | * @git-command git checkout $path 61 | * @param string $path 62 | * @throws BranchNotFound 63 | */ 64 | public function checkout(string $path, bool $merge = false): bool; 65 | 66 | /** 67 | * Cherry pick 68 | * @git-command git cherry-pick $commitSha 69 | * @throws BadRevision 70 | * @throws PreviousCherryPickIsNowEmpty 71 | */ 72 | public function cherryPick(string $commitSha): void; 73 | } 74 | -------------------------------------------------------------------------------- /src/Contracts/Commands/GitLogCommand.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | public function getList(): array; 31 | 32 | /** 33 | * Git apply stash 34 | * @git-command git apply stash stash@{$id} 35 | */ 36 | public function apply(int $id): bool; 37 | } 38 | -------------------------------------------------------------------------------- /src/Contracts/Commands/GitStatusCommand.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | public function getUntrackedFiles(): array; 28 | 29 | /** 30 | * Get modified files 31 | * @return array 32 | */ 33 | public function getModifiedFiles(): array; 34 | 35 | /** 36 | * Get added files 37 | * @return array 38 | */ 39 | public function getAddedFiles(): array; 40 | } 41 | -------------------------------------------------------------------------------- /src/Contracts/Commands/GitSubmoduleCommand.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public function getAll(): array; 25 | 26 | /** 27 | * Remove submodule 28 | * @throws FileNotFound 29 | * @throws SubmoduleNotFound 30 | */ 31 | public function remove(string $name): void; 32 | 33 | /** 34 | * Determine is exists submodule 35 | */ 36 | public function exists(string $name): bool; 37 | 38 | /** 39 | * Sync git submodule 40 | * @git-command git submodule sync $name 41 | */ 42 | public function sync(string $name): void; 43 | 44 | /** 45 | * Sync defines in .gitmodules from git config 46 | */ 47 | public function syncDefinesFromConfig(): void; 48 | } 49 | -------------------------------------------------------------------------------- /src/Contracts/Commands/GitTagCommand.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public function getAll(?string $pattern = null): array; 21 | 22 | /** 23 | * Add git tag 24 | * @git-command git -a $tag 25 | * @git-command git -a $tag -m $message 26 | * @throws TagAlreadyExists 27 | */ 28 | public function add(string $tag, ?string $message = null): bool; 29 | 30 | /** 31 | * Check tag exists 32 | */ 33 | public function exists(string $tag): bool; 34 | 35 | /** 36 | * Get git tag information 37 | * @git-command git show $tagName 38 | * @throws TagNotFound 39 | */ 40 | public function get(string $tagName): Tag; 41 | } 42 | -------------------------------------------------------------------------------- /src/Contracts/Commands/HasSubmodules.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | public function toArray(): array; 11 | } 12 | -------------------------------------------------------------------------------- /src/Contracts/Config/ConfigAttribute.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | public function getIterator(): iterable; 11 | } 12 | -------------------------------------------------------------------------------- /src/Contracts/Config/SubjectConfigurator.php: -------------------------------------------------------------------------------- 1 | $raw 9 | */ 10 | public function parse(array $raw): ConfigSubject; 11 | 12 | /** 13 | * @return string 14 | */ 15 | public function getPrefix(): string; 16 | } 17 | -------------------------------------------------------------------------------- /src/Contracts/Factory/GitHandlerFactory.php: -------------------------------------------------------------------------------- 1 | $domainMap 12 | */ 13 | public function urls(array $domainMap = []): GitUrl; 14 | } 15 | -------------------------------------------------------------------------------- /src/Contracts/Log/LogQuery.php: -------------------------------------------------------------------------------- 1 | 38 | */ 39 | public function getAvailableDomains(): array; 40 | 41 | /** 42 | * Create Data Object "Repo" by repository url 43 | * @throws GivenInvalidUri 44 | */ 45 | public function toRepoFromUrl(string $url): Repo; 46 | 47 | /** 48 | * Build url to "tags/branches compare" page on remote hosting 49 | */ 50 | public function toTagsCompareFromFetchUrl(string $fetchUrl, string $oneTag, string $twoTag): string; 51 | 52 | /** 53 | * Build url to file page on remote hosting 54 | */ 55 | public function toFileFromFetchUrl(string $fetchUrl, string $filePath, string $branch): string; 56 | } 57 | -------------------------------------------------------------------------------- /src/Contracts/PathGenerator.php: -------------------------------------------------------------------------------- 1 | name = $name; 21 | $this->email = $email; 22 | } 23 | 24 | public function equals(self $author): bool 25 | { 26 | return $this->name === $author->name && $this->email === $author->email; 27 | } 28 | 29 | public function __toString(): string 30 | { 31 | return "$this->name <$this->email>"; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Data/Author/CacheableHydrator.php: -------------------------------------------------------------------------------- 1 | */ 13 | protected $authors = []; 14 | 15 | public function __construct(AuthorHydrator $hydrator) 16 | { 17 | $this->hydrator = $hydrator; 18 | } 19 | 20 | public function hydrate(array $raw): Author 21 | { 22 | if (! array_key_exists($raw['email'], $this->authors)) { 23 | $this->authors[$raw['email']] = $this->hydrator->hydrate($raw); 24 | } 25 | 26 | return $this->authors[$raw['email']]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Data/Author/Hydrator.php: -------------------------------------------------------------------------------- 1 | hash = $hash; 17 | } 18 | 19 | public function getAbbreviatedHash(): string 20 | { 21 | return mb_substr($this->hash, 0, 6); 22 | } 23 | 24 | public function __toString(): string 25 | { 26 | return $this->hash; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Data/CommitsAuthor.php: -------------------------------------------------------------------------------- 1 | $commits 19 | */ 20 | public function __construct(Author $author, array $commits) 21 | { 22 | $this->author = $author; 23 | $this->commits = $commits; 24 | } 25 | 26 | /** 27 | * @param array> $data 28 | */ 29 | public static function fromArray( 30 | #[ArrayShape(['author' => Author::class, 'commits' => 'array'])] 31 | array $data 32 | ): self { 33 | return new self($data['author'], $data['commits']); 34 | } 35 | 36 | public function count(): int 37 | { 38 | return count($this->commits); 39 | } 40 | 41 | /** 42 | * @return iterable 43 | */ 44 | public function getIterator(): iterable 45 | { 46 | return new \ArrayIterator($this->commits); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Data/FileMatch.php: -------------------------------------------------------------------------------- 1 | file = $file; 22 | $this->line = $line; 23 | $this->content = $content; 24 | } 25 | 26 | /** 27 | * @param array $data 28 | */ 29 | public static function fromArray( 30 | #[ArrayShape(['file' => 'string', 'line' => 'int', 'content' => 'string'])] 31 | array $data 32 | ): self { 33 | return new self($data['file'], $data['line'], $data['content']); 34 | } 35 | 36 | public function getReference(): string 37 | { 38 | return $this->file . ':' . $this->line; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Data/GitAttributes.php: -------------------------------------------------------------------------------- 1 | $attributes 19 | */ 20 | public function __construct(string $pattern, array $attributes) 21 | { 22 | $this->pattern = $pattern; 23 | $this->attributes = $attributes; 24 | } 25 | 26 | public function has(string $attribute): bool 27 | { 28 | return in_array($attribute, $this->attributes); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Data/GitContext.php: -------------------------------------------------------------------------------- 1 | rootDir = $rootDir; 19 | $this->gitDir = $gitDir; 20 | } 21 | 22 | public static function make(string $rootDir, ?string $gitFolder = '.git'): self 23 | { 24 | return new self($rootDir, $rootDir . DIRECTORY_SEPARATOR . $gitFolder); 25 | } 26 | 27 | public function getRootDir(): string 28 | { 29 | return $this->rootDir; 30 | } 31 | 32 | public function getRootFolder(): string 33 | { 34 | return pathinfo($this->rootDir, PATHINFO_BASENAME); 35 | } 36 | 37 | public function getGitDir(): string 38 | { 39 | return $this->gitDir; 40 | } 41 | 42 | public function getRefsDir(): string 43 | { 44 | return $this->gitDir . DIRECTORY_SEPARATOR . 'refs'; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Data/Hook.php: -------------------------------------------------------------------------------- 1 | name = $name; 21 | $this->script = $script; 22 | $this->lastUpdateDate = $lastUpdateDate; 23 | } 24 | 25 | public static function now(string $name, string $script): self 26 | { 27 | return new self($name, $script, new \DateTime()); 28 | } 29 | 30 | public function isSample(): bool 31 | { 32 | return pathinfo($this->name, PATHINFO_EXTENSION) === 'sample'; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Data/Log.php: -------------------------------------------------------------------------------- 1 | commit = $commit; 25 | $this->author = $author; 26 | $this->date = $date; 27 | $this->message = $message; 28 | } 29 | 30 | public function equalsDate(\DateTimeInterface $date): bool 31 | { 32 | return $this->date->format('Y-m-d') === $date->format('Y-m-d'); 33 | } 34 | 35 | /** 36 | * @return array 37 | */ 38 | public function toArray(): array 39 | { 40 | return [ 41 | 'commit' => $this->commit->toArray(), 42 | 'date' => $this->date->format('Y-m-d H:i:s'), 43 | 'author' => $this->author->toArray(), 44 | 'message' => $this->message, 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Data/LogCollection.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | #[Immutable] 11 | class LogCollection implements \IteratorAggregate, \Countable 12 | { 13 | protected $logs; 14 | 15 | /** 16 | * @param non-empty-array $logs 17 | */ 18 | public function __construct(array $logs) 19 | { 20 | $this->logs = $logs; 21 | } 22 | 23 | /** 24 | * Get first Log from collection. 25 | */ 26 | public function first(): Log 27 | { 28 | return $this->logs[array_key_first($this->logs)]; 29 | } 30 | 31 | /** 32 | * Get last Log from collection. 33 | */ 34 | public function last(): Log 35 | { 36 | $logs = $this->logs; 37 | 38 | return end($logs); 39 | } 40 | 41 | public function filterByAuthorName(string $name): ?self 42 | { 43 | return $this->filter(function (Log $log) use ($name) { 44 | return $log->author->name === $name; 45 | }); 46 | } 47 | 48 | public function filterByDate(\DateTimeInterface $date): ?self 49 | { 50 | return $this->filter(function (Log $log) use ($date) { 51 | return $log->equalsDate($date); 52 | }); 53 | } 54 | 55 | public function filter(\Closure $callback): ?self 56 | { 57 | $logs = array_filter($this->logs, $callback); 58 | 59 | if (count($logs) === 0) { 60 | return null; 61 | } 62 | 63 | return new self(array_values($logs)); 64 | } 65 | 66 | /** 67 | * @return iterable 68 | */ 69 | public function getIterator(): iterable 70 | { 71 | return new \ArrayIterator($this->logs); 72 | } 73 | 74 | /** 75 | * Get count of logs. 76 | */ 77 | public function count(): int 78 | { 79 | return count($this->logs); 80 | } 81 | 82 | /** 83 | * @return Log[] 84 | */ 85 | public function all(): array 86 | { 87 | return $this->logs; 88 | } 89 | 90 | /** 91 | * @param array $aliases - emails 92 | * @return array 93 | */ 94 | public function getAuthorsWithCommits(array $aliases = []): array 95 | { 96 | $authors = []; 97 | 98 | foreach ($this->logs as $log) { 99 | $identity = $aliases[$log->author->email] ?? $log->author->email; 100 | 101 | if (! array_key_exists($identity, $authors)) { 102 | $authors[$identity]['author'] = $log->author; 103 | $authors[$identity]['commits'] = []; 104 | } 105 | 106 | $authors[$identity]['commits'][] = $log->commit; 107 | } 108 | 109 | return array_map([CommitsAuthor::class, 'fromArray'], $authors); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Data/Remotes.php: -------------------------------------------------------------------------------- 1 | fetch = $fetch; 22 | $this->push = $push; 23 | } 24 | 25 | public static function createEmpty(): self 26 | { 27 | return new self(Str::fromEmpty(), Str::fromEmpty()); 28 | } 29 | 30 | public function isEmpty(): bool 31 | { 32 | return $this->fetch->isEmpty() && $this->push->isEmpty(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Data/Repo.php: -------------------------------------------------------------------------------- 1 | name = $name; 24 | $this->user = $user; 25 | $this->url = $url; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Data/Stash.php: -------------------------------------------------------------------------------- 1 | id = $id; 24 | $this->branch = $branch; 25 | $this->name = $name; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Data/Submodule.php: -------------------------------------------------------------------------------- 1 | name = $name; 22 | $this->path = $path; 23 | $this->url = $url; 24 | } 25 | 26 | /** 27 | * @param array $array 28 | */ 29 | public static function fromArray( 30 | #[ArrayShape(['name' => 'string', 'path' => 'string', 'url' => 'string'])] 31 | array $array 32 | ): self { 33 | return new self($array['name'], $array['path'], $array['url']); 34 | } 35 | 36 | public function equals(self $other): bool 37 | { 38 | return $this->name === $other->name; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Data/Tag.php: -------------------------------------------------------------------------------- 1 | author = $author; 26 | $this->date = $date; 27 | $this->commit = $commit; 28 | $this->message = $message; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Data/Version.php: -------------------------------------------------------------------------------- 1 | full = $full; 23 | $this->major = $major; 24 | $this->minor = $minor; 25 | $this->patch = $patch; 26 | } 27 | 28 | public function __toString(): string 29 | { 30 | return $this->full; 31 | } 32 | 33 | /** 34 | * @param static|string $comparedVersion 35 | * @return bool|int 36 | */ 37 | public function compare($comparedVersion) 38 | { 39 | if ($comparedVersion instanceof static) { 40 | $comparedVersion = $comparedVersion->toTag(); 41 | } 42 | 43 | return version_compare($this->toTag(), $comparedVersion); 44 | } 45 | 46 | public function toTag(): string 47 | { 48 | return implode('.', [ 49 | $this->major, 50 | $this->minor, 51 | $this->patch, 52 | ]); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Enum/ArchiveFormat.php: -------------------------------------------------------------------------------- 1 | containsAny([ 16 | '[', '..', 17 | ]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Enum/ConfigSectionName.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public static function cases(): array 15 | { 16 | static $cases = []; 17 | 18 | if (! array_key_exists(static::class, $cases)) { 19 | $cases[static::class] = (new \ReflectionClass(static::class))->getConstants(); 20 | } 21 | 22 | return $cases[static::class]; 23 | } 24 | 25 | /** 26 | * @codeCoverageIgnore 27 | */ 28 | public static function from(string $value): self 29 | { 30 | if (! static::has($value)) { 31 | throw new \InvalidArgumentException('Invalid Enum Value'); 32 | } 33 | 34 | $enum = new self(); 35 | $enum->value = $value; 36 | 37 | return $enum; 38 | } 39 | 40 | /** 41 | * @codeCoverageIgnore 42 | */ 43 | protected static function has(string $value): bool 44 | { 45 | return array_key_exists($value, array_flip(static::cases())); 46 | } 47 | 48 | /** 49 | * @codeCoverageIgnore 50 | */ 51 | public function __toString() 52 | { 53 | return $this->value; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Enum/FormatPlaceholder.php: -------------------------------------------------------------------------------- 1 | $holders 36 | */ 37 | public static function format(array $holders, string $separator = '|'): string 38 | { 39 | return "format:'" . implode($separator, $holders) . "'"; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Enum/GarbageCollectMode.php: -------------------------------------------------------------------------------- 1 | errorBranch = $branch; 16 | 17 | parent::__construct('Already on '. $branch); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Exceptions/BadRevision.php: -------------------------------------------------------------------------------- 1 | failedRevision = $failedRevision; 12 | 13 | parent::__construct("Bad revision: $failedRevision"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Exceptions/BranchAlreadyExists.php: -------------------------------------------------------------------------------- 1 | errorBranch = $branch; 12 | 13 | $message = "Branch {$branch} already exists"; 14 | 15 | parent::__construct($message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Exceptions/BranchDoesNotHaveCommits.php: -------------------------------------------------------------------------------- 1 | errorBranch = $errorBranch; 12 | 13 | $message = "branch '$errorBranch' does not have any commits yet"; 14 | 15 | parent::__construct($message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Exceptions/BranchHasNoUpstream.php: -------------------------------------------------------------------------------- 1 | errorBranch = $branch; 12 | 13 | parent::__construct($this->prepareMessage()); 14 | } 15 | 16 | /** 17 | * @codeCoverageIgnore 18 | */ 19 | public static function patternStdError(): string 20 | { 21 | return "The current branch (.*) has no upstream branch"; 22 | } 23 | 24 | protected function prepareMessage(): string 25 | { 26 | return "The current branch $this->errorBranch has no upstream branch"; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Exceptions/BranchNotFound.php: -------------------------------------------------------------------------------- 1 | errorBranch = $branch; 12 | 13 | $message = "Git Branch '{$branch}' Not Found"; 14 | 15 | parent::__construct($message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Exceptions/CannotMergeAbort.php: -------------------------------------------------------------------------------- 1 | errorPrefix = $prefix; 12 | 13 | $message = "Config Data by prefix $prefix not found"; 14 | 15 | parent::__construct($message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Exceptions/ConfigSectionNotFound.php: -------------------------------------------------------------------------------- 1 | failedConfigSection = $failedConfigSection; 14 | 15 | parent::__construct("Key does not contain a section: $failedConfigSection"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Exceptions/ConfigVariableNotFound.php: -------------------------------------------------------------------------------- 1 | failedConfigVariable = $failedConfigVariable; 12 | 13 | parent::__construct("key does not contain a section: $failedConfigVariable"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Exceptions/FileNotFound.php: -------------------------------------------------------------------------------- 1 | invalidFilePath = $file; 14 | 15 | $message = "File '{$file}' Not Found"; 16 | 17 | parent::__construct($message); 18 | } 19 | 20 | public static function handleIfSo(Str $err): void 21 | { 22 | $path = $err->match("/pathspec '(.*)' did not match any/i"); 23 | if ($path->isNotEmpty()) { 24 | throw new self($path); 25 | } 26 | } 27 | 28 | public function getInvalidFilePath(): string 29 | { 30 | return $this->invalidFilePath; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Exceptions/GitHandlerException.php: -------------------------------------------------------------------------------- 1 | failedUrl = $url; 12 | 13 | parent::__construct("Given invalid uri: ". $url); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Exceptions/HookNotExists.php: -------------------------------------------------------------------------------- 1 | errorHookName = $errorHookName; 15 | 16 | parent::__construct("Hook $errorHookName not exists!"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Exceptions/MergeHeadMissing.php: -------------------------------------------------------------------------------- 1 | failedMergeBranch = $branch; 10 | 11 | parent::__construct("merge: $branch - not something we can merge"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Exceptions/NothingToCommit.php: -------------------------------------------------------------------------------- 1 | errorObjectName = $objectName; 16 | 17 | parent::__construct("Not a valid object name: '$objectName'"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Exceptions/OriginUrlNotFound.php: -------------------------------------------------------------------------------- 1 | errorPath = $path; 14 | 15 | $message = "Path '$path' already exists"; 16 | 17 | parent::__construct($message); 18 | } 19 | 20 | /** 21 | * @codeCoverageIgnore 22 | */ 23 | public static function patternStdError(string $path): string 24 | { 25 | return "destination path '{$path}' already exists"; 26 | } 27 | 28 | public static function handleIfSo(string $path, Str $stdout): void 29 | { 30 | if ($stdout->contains(static::patternStdError($path))) { 31 | throw new static($path); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Exceptions/PathIncorrect.php: -------------------------------------------------------------------------------- 1 | incorrectPath = $path; 12 | 13 | $message = "Path {$path} incorrect!"; 14 | 15 | parent::__construct($message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Exceptions/PathIsDirectoryNotCould.php: -------------------------------------------------------------------------------- 1 | errorReference = $errorReference; 12 | 13 | parent::__construct("Invalid reference: ". $errorReference); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Exceptions/RemoteAlreadyExists.php: -------------------------------------------------------------------------------- 1 | remoteName = $remoteName; 12 | 13 | $message = "Remote $remoteName already exists"; 14 | 15 | parent::__construct($message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Exceptions/RemoteNotFilled.php: -------------------------------------------------------------------------------- 1 | remoteName = $remoteName; 12 | 13 | parent::__construct("No such remote: $remoteName"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Exceptions/RemoteRepositoryNotFound.php: -------------------------------------------------------------------------------- 1 | errorRemoteRepository = $errorRemoteRepository; 12 | 13 | parent::__construct("Remote Repository $errorRemoteRepository not found"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Exceptions/RepositoryAlreadyExists.php: -------------------------------------------------------------------------------- 1 | errorStashId = $errorStashId; 12 | 13 | parent::__construct("Stash by id $errorStashId does not exists!"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Exceptions/SubjectConfiguratorNotFound.php: -------------------------------------------------------------------------------- 1 | errorTag = $errorTag; 15 | 16 | parent::__construct("Tag '$errorTag' not found!"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Exceptions/UnexpectedException.php: -------------------------------------------------------------------------------- 1 | errorCommand = $command; 12 | 13 | $message = "Unexpected exception after execution command: ". $command; 14 | 15 | parent::__construct($message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Exceptions/UnknownRevisionInWorkingTree.php: -------------------------------------------------------------------------------- 1 | failedRevisionOrPath = $revisionOrPath; 15 | 16 | parent::__construct("$revisionOrPath: unknown revision or path not in the working tree."); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Factory/CachedGitFactory.php: -------------------------------------------------------------------------------- 1 | factory = $factory; 16 | } 17 | 18 | public function factory(string $dir, string $bin = 'git'): GitHandler 19 | { 20 | return new CachedGit($this->factory->factory($dir, $bin)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Factory/DownloaderFactory.php: -------------------------------------------------------------------------------- 1 | > 12 | */ 13 | public function buildMap(string $content): array 14 | { 15 | if (empty(trim($content))) { 16 | return []; 17 | } 18 | 19 | $map = []; 20 | 21 | foreach (Str::make($content)->trim()->lines() as $match) { 22 | $parts = $match->deleteUnnecessarySpaces()->words(); 23 | 24 | $map[(string) $parts->first()] = $parts->slice(1)->toArray(); 25 | } 26 | 27 | return $map; 28 | } 29 | 30 | /** 31 | * @param non-empty-array> $map 32 | */ 33 | public function buildContent(array $map): string 34 | { 35 | $patterns = array_keys($map); 36 | $tabs = Tab::addSpaces($patterns); 37 | $content = ''; 38 | 39 | foreach ($patterns as $index => $pattern) { 40 | $content .= $tabs[$index] . implode(' ', $map[$pattern]); 41 | $content .= "\n"; 42 | } 43 | 44 | return $content; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Files/SubmodulesFile.php: -------------------------------------------------------------------------------- 1 | .*)"\]\s*path = (?.*)\n\s*url = (?.*)\n?#'; 11 | 12 | /** 13 | * @return array 14 | */ 15 | public function buildMap(string $content): array 16 | { 17 | $modules = []; 18 | 19 | foreach (Str::make($content)->globalMatch($this->regex) as $item) { 20 | $modules[$item['name']] = Submodule::fromArray($item); 21 | } 22 | 23 | return $modules; 24 | } 25 | 26 | /** 27 | * @param array $map 28 | */ 29 | public function buildContent(array $map): string 30 | { 31 | return implode('', array_map([$this, 'buildSubmoduleContent'], $map)); 32 | } 33 | 34 | protected function buildSubmoduleContent(Submodule $submodule): string 35 | { 36 | return <<name"] 38 | path = $submodule->path 39 | url = $submodule->url 40 | HTML; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Git.php: -------------------------------------------------------------------------------- 1 | fileSystem); 61 | } 62 | 63 | protected function createLogParser(): LogParser 64 | { 65 | return new Logger(new CacheableHydrator(new Hydrator())); 66 | } 67 | 68 | /** 69 | * @param array $domainMap 70 | */ 71 | public function urls(array $domainMap = []): GitUrl 72 | { 73 | $builders = [ 74 | new GithubOriginUrlBuilder($domainMap[GithubOriginUrlBuilder::NAME] ?? []), 75 | new GitlabOriginUrlBuilder($domainMap[GitlabOriginUrlBuilder::NAME] ?? []), 76 | new BitbucketOriginUrlBuilder($domainMap[BitbucketOriginUrlBuilder::NAME] ?? []), 77 | ]; 78 | 79 | $url = $this->remotes()->show()->fetch; 80 | 81 | return new GitUrl( 82 | OriginUrlSelector::make($builders)->selectByUrl($url), 83 | $url, 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Making/MakingPush.php: -------------------------------------------------------------------------------- 1 | remote = $remote; 23 | } 24 | 25 | public function onRemote(callable $setup): self 26 | { 27 | $this->remote = $setup($this->remote); 28 | 29 | return $this; 30 | } 31 | 32 | public function onBranch(string $branch): self 33 | { 34 | $this->branch = $branch; 35 | 36 | return $this; 37 | } 38 | 39 | public function onBranchHead(string $branch): self 40 | { 41 | return $this->onBranch('HEAD:' . $branch); 42 | } 43 | 44 | public function force(): self 45 | { 46 | $this->isForce = true; 47 | 48 | return $this; 49 | } 50 | 51 | public function onCurrentBranchHead(): self 52 | { 53 | return $this->onBranch('HEAD'); 54 | } 55 | 56 | public function onSetUpStream(): self 57 | { 58 | $this->setUpStream = true; 59 | 60 | return $this; 61 | } 62 | 63 | public function tags(): self 64 | { 65 | $this->tags = true; 66 | 67 | return $this; 68 | } 69 | 70 | public function buildCommand(ShellCommandInterface $command): ShellCommandInterface 71 | { 72 | return $command 73 | ->addArgument('push') 74 | ->addArgument($this->remote->__toString()) 75 | ->when($this->setUpStream === true, function (ShellCommandInterface $command) { 76 | $command->addCutOption('u'); 77 | }) 78 | ->when($this->branch !== null, function (ShellCommandInterface $command) { 79 | $command->addArgument($this->branch); 80 | }) 81 | ->when($this->isForce, function (ShellCommandInterface $command) { 82 | $command->addOption('force'); 83 | }) 84 | ->when($this->tags, function (ShellCommandInterface $command) { 85 | $command->addOption('tags'); 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Origin/Downloader.php: -------------------------------------------------------------------------------- 1 | url = $url; 28 | $this->client = $client; 29 | $this->fileSystem = $fileSystem; 30 | } 31 | 32 | /** 33 | * @throws \Psr\Http\Client\ClientExceptionInterface 34 | */ 35 | public function download(HasRemotes $git, string $pathToSave): bool 36 | { 37 | return $this->fileSystem->createFile($pathToSave, $this->fetch($git)); 38 | } 39 | 40 | /** 41 | * @throws \Psr\Http\Client\ClientExceptionInterface 42 | * @throws OriginUrlNotFound 43 | */ 44 | protected function fetch(HasRemotes $git): string 45 | { 46 | return $this->client->sendRequest($this->createRequestOnFetch($git))->getBody()->getContents(); 47 | } 48 | 49 | protected function createRequestOnFetch(HasRemotes $git): RequestInterface 50 | { 51 | return new Request('GET', $this->url->select($git)->toArchive($git)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Origin/Url/AbstractOriginUrlBuilder.php: -------------------------------------------------------------------------------- 1 | */ 15 | protected $domains = []; 16 | 17 | private const URL_REGEX = '/((git@|http(s)?:\/\/)(?[\w\.@]+)(\/|:))(?([\w,\-,\_]+))\/' . 18 | '(?([\w,\-,\_]+))(.git){0,1}((\/){0,1})/m'; 19 | 20 | /** 21 | * @param array $domains 22 | */ 23 | public function __construct(array $domains = []) 24 | { 25 | $this->domains = array_merge($this->domains, $domains); 26 | } 27 | 28 | public function toCommit(HasRemotes $git, string $hash): string 29 | { 30 | return $this->toCommitFromFetchUrl($git->remotes()->show()->fetch, $hash); 31 | } 32 | 33 | public function toArchive(HasRemotes $git, string $branch = 'master'): string 34 | { 35 | return $this->toArchiveFromFetchUrl($git->remotes()->show()->fetch, $branch); 36 | } 37 | 38 | public function getAvailableDomains(): array 39 | { 40 | return $this->domains; 41 | } 42 | 43 | public function toRepoFromUrl(string $url): Repo 44 | { 45 | $parsed = []; 46 | 47 | preg_match(self::URL_REGEX, $url, $parsed); 48 | 49 | if (! isset($parsed['repo']) || ! isset($parsed['owner']) || ! isset($parsed['host'])) { 50 | throw new GivenInvalidUri($url); 51 | } 52 | 53 | $newUrl = Uri::unParse([ 54 | 'scheme' => 'https', 55 | 'host' => $parsed['host'], 56 | 'path' => $parsed['owner'] . '/' . $parsed['repo'], 57 | ]); 58 | 59 | return new Repo($parsed['repo'], $parsed['owner'], $newUrl); 60 | } 61 | 62 | /** 63 | * @param Str|string $fetchUrl 64 | */ 65 | protected function toGitFolder($fetchUrl): Str 66 | { 67 | if (is_string($fetchUrl)) { 68 | $fetchUrl = Str::make($fetchUrl); 69 | } 70 | 71 | return $fetchUrl->delete(['.git']); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Origin/Url/BitbucketOriginUrlBuilder.php: -------------------------------------------------------------------------------- 1 | */ 10 | protected $domains = [ 11 | 'bitbucket.org', 12 | ]; 13 | 14 | public function toCommitFromFetchUrl(string $fetchUrl, string $hash): string 15 | { 16 | return $this->toGitFolder($fetchUrl)->append('/commits/'. $hash); 17 | } 18 | 19 | public function toArchiveFromFetchUrl(string $fetchUrl, string $branch = 'master'): string 20 | { 21 | return $this->toGitFolder($fetchUrl)->append("/get/$branch.zip"); 22 | } 23 | 24 | public function toTagFromFetchUrl(string $fetchUrl, string $tag): string 25 | { 26 | return $this->toGitFolder($fetchUrl)->append('/src/')->append($tag); 27 | } 28 | 29 | public function toTagsCompareFromFetchUrl(string $fetchUrl, string $oneTag, string $twoTag): string 30 | { 31 | return $this->toGitFolder($fetchUrl)->append("/branches/compare/$oneTag%0D$twoTag"); 32 | } 33 | 34 | public function toFileFromFetchUrl(string $fetchUrl, string $filePath, string $branch): string 35 | { 36 | return $this->toGitFolder($fetchUrl)->append("/src/$branch/$filePath"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Origin/Url/GitUrl.php: -------------------------------------------------------------------------------- 1 | builder = $builder; 21 | $this->fetchUrl = $fetchUrl; 22 | } 23 | 24 | public function toCommit(string $hash): string 25 | { 26 | return $this->builder->toCommitFromFetchUrl($this->fetchUrl, $hash); 27 | } 28 | 29 | public function toArchive(string $branch = 'master'): string 30 | { 31 | return $this->builder->toArchiveFromFetchUrl($this->fetchUrl, $branch); 32 | } 33 | 34 | public function toTag(string $tag): string 35 | { 36 | return $this->builder->toTagFromFetchUrl($this->fetchUrl, $tag); 37 | } 38 | 39 | public function toTagsCompare(string $oneTag, string $twoTag): string 40 | { 41 | return $this->builder->toTagsCompareFromFetchUrl($this->fetchUrl, $oneTag, $twoTag); 42 | } 43 | 44 | public function toFile(string $filePath, string $branch): string 45 | { 46 | return $this->builder->toFileFromFetchUrl($this->fetchUrl, $filePath, $branch); 47 | } 48 | 49 | public function toRepo(): Repo 50 | { 51 | return $this->builder->toRepoFromUrl($this->fetchUrl); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Origin/Url/GithubOriginUrlBuilder.php: -------------------------------------------------------------------------------- 1 | */ 15 | protected $domains = [ 16 | 'github.com', 17 | ]; 18 | 19 | public function toCommitFromFetchUrl(string $fetchUrl, string $hash): string 20 | { 21 | return $this->toGitFolder($fetchUrl)->append('/commit/'. $hash); 22 | } 23 | 24 | public function toArchiveFromFetchUrl(string $fetchUrl, string $branch = 'master'): string 25 | { 26 | $host = Uri::host($fetchUrl); 27 | 28 | return $this 29 | ->toGitFolder($fetchUrl) 30 | ->replace([ 31 | $host => $this->buildArchiveDomain($host), 32 | ]) 33 | ->append("/zip/refs/heads/$branch"); 34 | } 35 | 36 | public function toTagFromFetchUrl(string $fetchUrl, string $tag): string 37 | { 38 | return $this->toGitFolder($fetchUrl)->append('/releases/tag/')->append($tag); 39 | } 40 | 41 | public function toTagsCompareFromFetchUrl(string $fetchUrl, string $oneTag, string $twoTag): string 42 | { 43 | return $this->toGitFolder($fetchUrl)->append("/compare/$oneTag...$twoTag"); 44 | } 45 | 46 | public function toFileFromFetchUrl(string $fetchUrl, string $filePath, string $branch): string 47 | { 48 | return $this->toGitFolder($fetchUrl)->append("/blob/$branch/$filePath"); 49 | } 50 | 51 | protected function buildArchiveDomain(string $host): string 52 | { 53 | return $this->archiveSubdomain . '.' . $host; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Origin/Url/GitlabOriginUrlBuilder.php: -------------------------------------------------------------------------------- 1 | */ 12 | protected $domains = [ 13 | 'gitlab.com', 14 | ]; 15 | 16 | public function toCommitFromFetchUrl(string $fetchUrl, string $hash): string 17 | { 18 | return $this->toGitFolder($fetchUrl)->append('/-/commit/'. $hash); 19 | } 20 | 21 | public function toArchiveFromFetchUrl(string $fetchUrl, string $branch = 'master'): string 22 | { 23 | $folder = $this->toGitFolder($fetchUrl); 24 | $repoName = pathinfo($folder, PATHINFO_BASENAME); 25 | 26 | return $folder->append("/-/archive/$branch/$repoName-$branch.zip"); 27 | } 28 | 29 | public function toTagFromFetchUrl(string $fetchUrl, string $tag): string 30 | { 31 | return $this->toGitFolder($fetchUrl)->append('/-/tags/'. $tag); 32 | } 33 | 34 | public function toTagsCompareFromFetchUrl(string $fetchUrl, string $oneTag, string $twoTag): string 35 | { 36 | return $this->toGitFolder($fetchUrl)->append("-/compare/$oneTag...$twoTag"); 37 | } 38 | 39 | public function toFileFromFetchUrl(string $fetchUrl, string $filePath, string $branch): string 40 | { 41 | return $this->toGitFolder($fetchUrl)->append("/-/blob/$branch/$filePath"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Origin/Url/OriginUrlSelector.php: -------------------------------------------------------------------------------- 1 | $map 16 | * @codeCoverageIgnore 17 | */ 18 | public function __construct(array $map) 19 | { 20 | $this->map = $map; 21 | } 22 | 23 | /** 24 | * @param array $urls 25 | */ 26 | public static function make(array $urls): self 27 | { 28 | $map = []; 29 | 30 | foreach ($urls as $url) { 31 | foreach ($url->getAvailableDomains() as $domain) { 32 | $map[$domain] = $url; 33 | } 34 | } 35 | 36 | return new self($map); 37 | } 38 | 39 | /** 40 | * @throws OriginUrlNotFound 41 | */ 42 | public function select(HasRemotes $git): OriginUrlBuilder 43 | { 44 | return $this->selectByUrl($git->remotes()->show()->fetch); 45 | } 46 | 47 | public function selectByUrl(string $url): OriginUrlBuilder 48 | { 49 | return $this->selectByDomain(Uri::host($url)); 50 | } 51 | 52 | /** 53 | * @throws OriginUrlNotFound 54 | */ 55 | public function selectByDomain(string $domain): OriginUrlBuilder 56 | { 57 | if (! $this->has($domain)) { 58 | throw new OriginUrlNotFound(); 59 | } 60 | 61 | return $this->map[$domain]; 62 | } 63 | 64 | public function has(string $domain): bool 65 | { 66 | return array_key_exists($domain, $this->map); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Support/Chmod.php: -------------------------------------------------------------------------------- 1 | addArgument('+x')->addArgument($path); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Support/LocalFileSystem.php: -------------------------------------------------------------------------------- 1 | exists($path)) { 13 | throw new FileNotFound($path); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Support/LogBuilder.php: -------------------------------------------------------------------------------- 1 | */ 12 | protected $filenames = []; 13 | 14 | /** @var list */ 15 | protected $authors = []; 16 | 17 | /** @var array */ 18 | protected $unions = []; 19 | 20 | /** @var array> */ 21 | protected $optionValues = []; 22 | 23 | /** @var array */ 24 | protected $diff = null; 25 | 26 | public function offset(int $offset): self 27 | { 28 | if ($offset === 0) { 29 | return $this; 30 | } 31 | 32 | return $this->setOptionValue('skip', (string) $offset); 33 | } 34 | 35 | public function limit(int $limit): self 36 | { 37 | return $this->setOptionValue('max-count', (string) $limit); 38 | } 39 | 40 | public function before(\DateTimeInterface $date): self 41 | { 42 | return $this->setOptionValueDate('before', $date); 43 | } 44 | 45 | public function after(\DateTimeInterface $date): self 46 | { 47 | return $this->setOptionValueDate('after', $date); 48 | } 49 | 50 | public function file(string $filename): self 51 | { 52 | $this->filenames[] = $filename; 53 | 54 | return $this; 55 | } 56 | 57 | public function author(string $author): self 58 | { 59 | $this->authors[] = $author; 60 | 61 | return $this; 62 | } 63 | 64 | public function grep(string $pattern): self 65 | { 66 | $this->optionValues['grep'][] = $pattern; 67 | 68 | return $this; 69 | } 70 | 71 | public function union(callable $build): self 72 | { 73 | $this->unions[] = [$build, new self()]; 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * @return $this 80 | */ 81 | public function diff(string $src, string $dest) 82 | { 83 | $this->diff = [$src, $dest]; 84 | 85 | return $this; 86 | } 87 | 88 | public function build(ShellCommandInterface $command): ShellCommandInterface 89 | { 90 | $pureCommand = clone $command; 91 | 92 | return $command 93 | ->when(count($this->authors) > 0, function (ShellCommandInterface $command) { 94 | $command->addOptionWithValue('author', implode('|', $this->authors), true); 95 | }) 96 | ->addArguments($this->filenames) 97 | ->when($this->diff !== null, function (ShellCommandInterface $command) { 98 | $command->addArgument($this->diff[0] . '..' . $this->diff[1], false); 99 | }) 100 | ->when(count($this->optionValues) > 0, function (ShellCommandInterface $command) { 101 | foreach ($this->optionValues as $option => $values) { 102 | foreach ($values as $value) { 103 | $command->addOptionWithValue($option, $value, true); 104 | } 105 | } 106 | }) 107 | ->when(count($this->unions) > 0, function (ShellCommandInterface $command) use ($pureCommand) { 108 | foreach ($this->unions as [$callback, $builder]) { 109 | $callback($builder); 110 | 111 | $command->joinAnd($builder->build($pureCommand)); 112 | } 113 | }); 114 | } 115 | 116 | protected function setOptionValueDate(string $option, \DateTimeInterface $date): self 117 | { 118 | return $this->setOptionValue($option, $date->format('Y-m-d H:i:s')); 119 | } 120 | 121 | protected function setOptionValue(string $option, string $value): self 122 | { 123 | $this->optionValues[$option][0] = $value; 124 | 125 | return $this; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Support/Logger.php: -------------------------------------------------------------------------------- 1 | authorHydrator = $authorHydrator; 22 | } 23 | 24 | public function parse(Str $raw): ?LogCollection 25 | { 26 | $logs = []; 27 | 28 | foreach ($raw->globalMatch($this->regex) as $match) { 29 | $logs[] = new Log( 30 | new Commit(trim($match[1])), 31 | new \DateTime($match[2]), 32 | $this->createAuthor($match), 33 | trim($match[5]) 34 | ); 35 | } 36 | 37 | if (count($logs) === 0) { 38 | return null; 39 | } 40 | 41 | return new LogCollection($logs); 42 | } 43 | 44 | /** 45 | * @param array $raw 46 | */ 47 | protected function createAuthor(array $raw): Author 48 | { 49 | return $this->authorHydrator->hydrate([ 50 | 'name' => $raw[3], 51 | 'email' => $raw[4], 52 | ]); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Support/SimpleHttpClient.php: -------------------------------------------------------------------------------- 1 | getUri()->__toString()); 15 | 16 | $status = $content ? 200 : 404; 17 | 18 | return new Response($status, [], $content); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Support/TemporaryPathGenerator.php: -------------------------------------------------------------------------------- 1 | files = $files; 18 | } 19 | 20 | public function toArchive(ArchiveFormat $format): string 21 | { 22 | return $this->files->getTmpDir() 23 | . DIRECTORY_SEPARATOR 24 | . $this->buildArchiveName() 25 | . '.' . $format->value; 26 | } 27 | 28 | protected function buildArchiveName(): string 29 | { 30 | return implode('-', [ 31 | 'git', 32 | 'handler', 33 | time(), 34 | 'archive', 35 | static::$counter++, 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Support/ToArray.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | public function toArray(): array 11 | { 12 | return get_object_vars($this); 13 | } 14 | 15 | public function isEmpty(): bool 16 | { 17 | return count(array_filter($this->toArray())) === 0; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Support/TypeCaster.php: -------------------------------------------------------------------------------- 1 | $raw 14 | */ 15 | public static function string(array $raw, string $key): string 16 | { 17 | return array_key_exists($key, $raw) ? $raw[$key] : ''; 18 | } 19 | 20 | /** 21 | * @param array $raw 22 | */ 23 | public static function integer(array $raw, string $key, int $default = 0): int 24 | { 25 | return array_key_exists($key, $raw) ? (int) $raw[$key] : $default; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Support/Uri.php: -------------------------------------------------------------------------------- 1 | $parts 13 | */ 14 | public static function unParse( 15 | #[ArrayShape(['scheme' => 'string', 'host' => 'string', 'path' => 'string'])] 16 | array $parts 17 | ): string { 18 | return implode('/', array_filter([ 19 | array_key_exists('scheme', $parts) ? $parts['scheme'] . ':/' : null, 20 | ($parts['host'] ?? ''), 21 | $parts['path'], 22 | ])); 23 | } 24 | 25 | public static function host(string $uri): string 26 | { 27 | $host = parse_url($uri, PHP_URL_HOST); 28 | 29 | if ($host !== null) { 30 | return $host; 31 | } 32 | 33 | $find = Str::make($uri) 34 | ->trim() 35 | ->match('/([a-zA-Z0-9\-\.]+)\.(com|org|net|mil|edu|ru|COM|ORG|NET|MIL|EDU|RU)/', 0, 0, false); 36 | 37 | if ($find->isNotEmpty()) { 38 | return $find; 39 | } 40 | 41 | throw new GivenInvalidUri($uri); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Transactions/ArchiveTransaction.php: -------------------------------------------------------------------------------- 1 | context = $context; 29 | $this->git = $git; 30 | $this->files = $files; 31 | $this->paths = $paths; 32 | } 33 | 34 | public function attempt(callable $callback) 35 | { 36 | $archivePath = $this->paths->toArchive(ArchiveFormat::from(ArchiveFormat::ZIP)); 37 | 38 | $this->git->archives()->packRefs($archivePath); 39 | 40 | try { 41 | $result = $callback($this->git); 42 | 43 | $this->files->removeFile($archivePath); 44 | 45 | return $result; 46 | } catch (\Throwable $e) { 47 | $this->files->removeDir($this->context->getRefsDir()); 48 | $this->git->archives()->unPackRefs($archivePath); 49 | $this->git->garbage()->collect(GarbageCollectMode::from(GarbageCollectMode::AUTO)); 50 | 51 | throw $e; 52 | } 53 | } 54 | } 55 | --------------------------------------------------------------------------------