(Grav GitSync) Automatic Commit from GitSync
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace Grav\Plugin\GitSync;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Plugin;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Plugin\Admin\AdminBaseController;
|
||||
|
||||
class AdminController extends AdminBaseController
|
||||
{
|
||||
protected $action;
|
||||
protected $target;
|
||||
protected $active;
|
||||
protected $plugin;
|
||||
protected $task_prefix = 'task';
|
||||
|
||||
/** @var GitSync */
|
||||
public $git;
|
||||
|
||||
/**
|
||||
* @param Plugin $plugin
|
||||
*/
|
||||
public function __construct(Plugin $plugin)
|
||||
{
|
||||
$this->grav = Grav::instance();
|
||||
$this->active = false;
|
||||
$uri = $this->grav['uri'];
|
||||
$this->plugin = $plugin;
|
||||
|
||||
$post = !empty($_POST) ? $_POST : [];
|
||||
$this->post = $this->getPost($post);
|
||||
|
||||
// Ensure the controller should be running
|
||||
if (Utils::isAdminPlugin()) {
|
||||
$routeDetails = $this->grav['admin']->getRouteDetails();
|
||||
$target = array_pop($routeDetails);
|
||||
$this->git = new GitSync();
|
||||
|
||||
// return null if this is not running
|
||||
if ($target !== $plugin->name) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->action = !empty($this->post['action']) ? $this->post['action'] : $uri->param('action');
|
||||
$this->target = $target;
|
||||
$this->active = true;
|
||||
$this->admin = Grav::instance()['admin'];
|
||||
|
||||
$task = !empty($post['task']) ? $post['task'] : $uri->param('task');
|
||||
if ($task && ($this->target === $plugin->name || $uri->route() === '/lessons')) {
|
||||
$this->task = $task;
|
||||
$this->active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function taskTestConnection()
|
||||
{
|
||||
$post = $this->post;
|
||||
$test = base64_decode($post['test']) ?: null;
|
||||
$data = $test ? json_decode($test, false) : new \stdClass();
|
||||
|
||||
try {
|
||||
$testResult = Helper::testRepository($data->user, $data->password, $data->repository, $data->branch);
|
||||
|
||||
if (!empty($testResult)) {
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'The connection to the repository has been successful.'
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Branch "' . $data->branch .'" not found in the repository.'
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$invalid = str_replace($data->password, '{password}', $e->getMessage());
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => $invalid
|
||||
]);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
public function taskSynchronize()
|
||||
{
|
||||
try {
|
||||
$this->plugin->synchronize();
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'GitSync has successfully synchronized with the repository.'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$invalid = str_replace($this->git->getConfig('password', null), '{password}', $e->getMessage());
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => $invalid
|
||||
]);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
public function taskResetLocal()
|
||||
{
|
||||
try {
|
||||
$this->plugin->reset();
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'GitSync has successfully reset your local changes and synchronized with the repository.'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$invalid = str_replace($this->git->getConfig('password', null), '{password}', $e->getMessage());
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => $invalid
|
||||
]);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a task or action on a post or target.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function execute()
|
||||
{
|
||||
$params = [];
|
||||
|
||||
// Handle Task & Action
|
||||
if ($this->post && $this->task) {
|
||||
// validate nonce
|
||||
if (!$this->validateNonce()) {
|
||||
return false;
|
||||
}
|
||||
$method = $this->task_prefix . ucfirst($this->task);
|
||||
} elseif ($this->target) {
|
||||
if (!$this->action) {
|
||||
return false;
|
||||
}
|
||||
$method = strtolower($this->action) . ucfirst($this->target);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!method_exists($this, $method)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$success = $this->{$method}(...$params);
|
||||
|
||||
// Grab redirect parameter.
|
||||
$redirect = $this->post['_redirect'] ?? null;
|
||||
unset($this->post['_redirect']);
|
||||
|
||||
// Redirect if requested.
|
||||
if ($redirect) {
|
||||
$this->setRedirect($redirect);
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isActive()
|
||||
{
|
||||
return (bool) $this->active;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Grav\Plugin\GitSync\Api;
|
||||
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
use Grav\Common\Plugins;
|
||||
use Grav\Plugin\Api\Controllers\AbstractApiController;
|
||||
use Grav\Plugin\Api\Exceptions\ForbiddenException;
|
||||
use Grav\Plugin\Api\Exceptions\ValidationException;
|
||||
use Grav\Plugin\Api\Response\ApiResponse;
|
||||
use Grav\Plugin\GitSync\GitSync;
|
||||
use Grav\Plugin\GitSync\Helper;
|
||||
use Grav\Plugin\GitSyncPlugin;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Admin-Next API controller for git-sync.
|
||||
*
|
||||
* Endpoints back the blueprint-mode plugin settings page plus the wizard
|
||||
* modal hosted by the auto-loaded floating widget script. Settings are
|
||||
* persisted to config://plugins/git-sync.yaml — the same file admin-classic
|
||||
* reads/writes — so the two admins stay interchangeable.
|
||||
*/
|
||||
class GitSyncApiController extends AbstractApiController
|
||||
{
|
||||
private function requireGitSyncPermission(ServerRequestInterface $request, string $level): void
|
||||
{
|
||||
$user = $this->getUser($request);
|
||||
|
||||
if ($this->isSuperAdmin($user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->hasPermission($user, 'api.access')) {
|
||||
throw new ForbiddenException('API access is not enabled for this user.');
|
||||
}
|
||||
|
||||
$required = $level === 'write'
|
||||
? ['api.git-sync', 'api.git-sync.write', 'api.git-sync.admin']
|
||||
: ['api.git-sync', 'api.git-sync.read', 'api.git-sync.write', 'api.git-sync.admin'];
|
||||
|
||||
foreach ($required as $perm) {
|
||||
if ($this->hasPermission($user, $perm)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ForbiddenException("Missing required Git Sync '{$level}' permission");
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /git-sync/data — current settings for the plugin form.
|
||||
*
|
||||
* The raw encrypted password never leaves the server — we only signal
|
||||
* whether one is stored, so the enc-password field can show its
|
||||
* "securely stored" placeholder.
|
||||
*/
|
||||
public function data(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->requireGitSyncPermission($request, 'read');
|
||||
|
||||
$cfg = (array) $this->config->get('plugins.git-sync', []);
|
||||
$sync = (array) ($cfg['sync'] ?? []);
|
||||
$remote = (array) ($cfg['remote'] ?? []);
|
||||
$git = (array) ($cfg['git'] ?? []);
|
||||
|
||||
return ApiResponse::create([
|
||||
'enabled' => (bool) ($cfg['enabled'] ?? false),
|
||||
'folders' => array_values((array) ($cfg['folders'] ?? ['pages'])),
|
||||
'local_repository' => (string) ($cfg['local_repository'] ?? ''),
|
||||
'repository' => (string) ($cfg['repository'] ?? ''),
|
||||
'no_user' => (bool) ($cfg['no_user'] ?? false),
|
||||
'user' => (string) ($cfg['user'] ?? ''),
|
||||
// Form binds an empty string by default; server keeps the existing
|
||||
// password unless the user types a new one. Storage state is
|
||||
// exposed on the resolved blueprint (see onApiBlueprintResolved)
|
||||
// so the enc-password component can render the right placeholder.
|
||||
'password' => '',
|
||||
'webhook' => (string) ($cfg['webhook'] ?? ''),
|
||||
'webhook_enabled' => (bool) ($cfg['webhook_enabled'] ?? false),
|
||||
'webhook_secret' => (string) ($cfg['webhook_secret'] ?? ''),
|
||||
'branch' => (string) ($cfg['branch'] ?? 'master'),
|
||||
'logging' => (bool) ($cfg['logging'] ?? false),
|
||||
'sync' => [
|
||||
'direction' => (string) ($sync['direction'] ?? 'both'),
|
||||
'on_save' => (bool) ($sync['on_save'] ?? true),
|
||||
'on_delete' => (bool) ($sync['on_delete'] ?? true),
|
||||
'on_media' => (bool) ($sync['on_media'] ?? true),
|
||||
'cron_enable' => (bool) ($sync['cron_enable'] ?? false),
|
||||
'cron_at' => (string) ($sync['cron_at'] ?? '0 12,23 * * *'),
|
||||
],
|
||||
'remote' => [
|
||||
'name' => (string) ($remote['name'] ?? 'origin'),
|
||||
'branch' => (string) ($remote['branch'] ?? 'master'),
|
||||
],
|
||||
'git' => [
|
||||
'author' => (string) ($git['author'] ?? 'gituser'),
|
||||
'message' => (string) ($git['message'] ?? '(Grav GitSync) Automatic Commit'),
|
||||
'name' => (string) ($git['name'] ?? 'GitSync'),
|
||||
'email' => (string) ($git['email'] ?? 'git-sync@trilby.media'),
|
||||
'bin' => (string) ($git['bin'] ?? 'git'),
|
||||
'ignore' => (string) ($git['ignore'] ?? ''),
|
||||
'private_key' => (string) ($git['private_key'] ?? ''),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /git-sync/data — persist plugin settings.
|
||||
*
|
||||
* Mirrors the admin-classic onAdminSave password handling: if the form
|
||||
* sends an empty password we keep whatever is currently stored (and
|
||||
* encrypt it if it was somehow saved in plaintext); a non-empty value
|
||||
* gets encrypted before write.
|
||||
*/
|
||||
public function save(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->requireGitSyncPermission($request, 'write');
|
||||
|
||||
$body = $this->getRequestBody($request);
|
||||
$existing = (array) $this->config->get('plugins.git-sync', []);
|
||||
$merged = $existing;
|
||||
|
||||
// Top-level scalars / lists
|
||||
foreach (['enabled', 'folders', 'local_repository', 'repository', 'no_user',
|
||||
'user', 'webhook', 'webhook_enabled', 'webhook_secret', 'branch', 'logging'] as $key) {
|
||||
if (array_key_exists($key, $body)) {
|
||||
$merged[$key] = $body[$key];
|
||||
}
|
||||
}
|
||||
|
||||
// Password — empty means "keep existing", non-empty means "encrypt & replace"
|
||||
$newPassword = $body['password'] ?? null;
|
||||
if ($newPassword === null || $newPassword === '') {
|
||||
$current = (string) ($existing['password'] ?? '');
|
||||
if ($current !== '' && !str_starts_with($current, 'gitsync-')) {
|
||||
$merged['password'] = Helper::encrypt($current);
|
||||
} else {
|
||||
$merged['password'] = $current;
|
||||
}
|
||||
} else {
|
||||
$merged['password'] = Helper::encrypt((string) $newPassword);
|
||||
}
|
||||
|
||||
// Nested: sync / remote / git
|
||||
foreach (['sync', 'remote', 'git'] as $section) {
|
||||
if (isset($body[$section]) && is_array($body[$section])) {
|
||||
$merged[$section] = array_merge((array) ($merged[$section] ?? []), $body[$section]);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-generate webhook / webhook_secret if blank, matching admin-classic's data-default@
|
||||
if (empty($merged['webhook'])) {
|
||||
$merged['webhook'] = GitSyncPlugin::generateRandomWebhook();
|
||||
}
|
||||
if (empty($merged['webhook_secret'])) {
|
||||
$merged['webhook_secret'] = GitSyncPlugin::generateWebhookSecret();
|
||||
}
|
||||
|
||||
$this->writePluginConfig($merged);
|
||||
|
||||
// Mirror admin-classic onAdminAfterSave: initialize repo / set remote
|
||||
// when the plugin page form is saved with a configured repository.
|
||||
if (Helper::isGitInstalled() && Helper::isGitSyncConfigured()) {
|
||||
try {
|
||||
$git = new GitSync();
|
||||
$git->setConfig($merged);
|
||||
$git->initializeRepository();
|
||||
$git->setUser();
|
||||
$git->addRemote();
|
||||
} catch (\Throwable $e) {
|
||||
// Don't fail the save — surface as a warning in the response.
|
||||
return ApiResponse::create([
|
||||
'message' => 'Settings saved, but repository setup ran into an issue: '
|
||||
. Helper::preventReadablePassword($e->getMessage(), $merged['password'] ?? ''),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return ApiResponse::create([
|
||||
'message' => 'Git Sync settings saved.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /git-sync/sync — synchronize with the remote repository.
|
||||
*/
|
||||
public function sync(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->requireGitSyncPermission($request, 'write');
|
||||
|
||||
if (!Helper::isGitInstalled()) {
|
||||
throw new ValidationException('Git is not installed or not on the configured PATH.');
|
||||
}
|
||||
if (!Helper::isGitSyncReady()) {
|
||||
throw new ValidationException('Git Sync is not configured yet — run the Wizard first.');
|
||||
}
|
||||
|
||||
@set_time_limit(0);
|
||||
// Release the PHP session lock so the rest of admin-next stays
|
||||
// responsive while the network-bound git pull/push finishes.
|
||||
// Without this, every concurrent request from the same browser
|
||||
// blocks behind this one and the UI feels frozen.
|
||||
@session_write_close();
|
||||
|
||||
try {
|
||||
$plugin = $this->getGitSyncPlugin();
|
||||
$plugin->synchronize();
|
||||
} catch (\Throwable $e) {
|
||||
$password = (string) ($this->config->get('plugins.git-sync.password') ?? '');
|
||||
throw new ValidationException(
|
||||
Helper::preventReadablePassword($e->getMessage(), $password)
|
||||
);
|
||||
}
|
||||
|
||||
return ApiResponse::create([
|
||||
'message' => 'Git Sync has successfully synchronized with the repository.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /git-sync/reset — discard local changes (git reset --hard HEAD).
|
||||
*/
|
||||
public function reset(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->requireGitSyncPermission($request, 'write');
|
||||
|
||||
if (!Helper::isGitInstalled()) {
|
||||
throw new ValidationException('Git is not installed or not on the configured PATH.');
|
||||
}
|
||||
if (!Helper::isGitSyncReady()) {
|
||||
throw new ValidationException('Git Sync is not configured yet — run the Wizard first.');
|
||||
}
|
||||
|
||||
@set_time_limit(0);
|
||||
@session_write_close();
|
||||
|
||||
try {
|
||||
$plugin = $this->getGitSyncPlugin();
|
||||
$plugin->reset();
|
||||
} catch (\Throwable $e) {
|
||||
$password = (string) ($this->config->get('plugins.git-sync.password') ?? '');
|
||||
throw new ValidationException(
|
||||
Helper::preventReadablePassword($e->getMessage(), $password)
|
||||
);
|
||||
}
|
||||
|
||||
return ApiResponse::create([
|
||||
'message' => 'Git Sync has reset your local copy and re-synchronized with the repository.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /git-sync/test-connection — wizard "Verify Authentication, Connection and Branch".
|
||||
*
|
||||
* Body: { user, password, repository, branch, no_user }
|
||||
*
|
||||
* Mirrors AdminController::taskTestConnection. The credentials are NOT
|
||||
* persisted — they exist only for the duration of this ls-remote probe.
|
||||
*/
|
||||
public function testConnection(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->requireGitSyncPermission($request, 'write');
|
||||
|
||||
if (!Helper::isGitInstalled()) {
|
||||
throw new ValidationException('Git is not installed or not on the configured PATH.');
|
||||
}
|
||||
|
||||
$body = $this->getRequestBody($request);
|
||||
$user = (string) ($body['user'] ?? '');
|
||||
$password = (string) ($body['password'] ?? '');
|
||||
$repository = (string) ($body['repository'] ?? '');
|
||||
$branch = (string) ($body['branch'] ?? '');
|
||||
$noUser = (bool) ($body['no_user'] ?? false);
|
||||
|
||||
if ($repository === '') {
|
||||
throw new ValidationException('Repository URL is required.');
|
||||
}
|
||||
if ($branch === '') {
|
||||
throw new ValidationException('Branch is required.');
|
||||
}
|
||||
if ($noUser) {
|
||||
$user = '';
|
||||
}
|
||||
|
||||
try {
|
||||
$result = Helper::testRepository($user, $password, $repository, $branch);
|
||||
} catch (\Throwable $e) {
|
||||
$message = str_replace($password, '{password}', $e->getMessage());
|
||||
return ApiResponse::create([
|
||||
'status' => 'error',
|
||||
'message' => $message,
|
||||
]);
|
||||
}
|
||||
|
||||
if (empty($result)) {
|
||||
return ApiResponse::create([
|
||||
'status' => 'error',
|
||||
'message' => "Branch \"{$branch}\" not found in the repository.",
|
||||
]);
|
||||
}
|
||||
|
||||
return ApiResponse::create([
|
||||
'status' => 'success',
|
||||
'message' => 'Connection to the repository was successful.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /git-sync/wizard/state — pre-flight + current settings for the wizard.
|
||||
*
|
||||
* The wizard reuses the saved repo / branch / webhook to pre-fill its
|
||||
* inputs the second time around (admin-classic does the same via Twig).
|
||||
*/
|
||||
public function wizardState(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->requireGitSyncPermission($request, 'read');
|
||||
|
||||
$cfg = (array) $this->config->get('plugins.git-sync', []);
|
||||
$password = (string) ($cfg['password'] ?? '');
|
||||
|
||||
// Compute the public site URL the way Twig admin-classic does it
|
||||
// (`uri.base ~ uri.rootUrl`). The browser-side `window.location.origin`
|
||||
// alone misses Grav installs that live in a sub-folder, leaving the
|
||||
// wizard's webhook URL preview wrong. Trailing slash trimmed so the
|
||||
// client can append the webhook path cleanly.
|
||||
$uri = $this->grav['uri'];
|
||||
$frontendUrl = rtrim($uri->base() . $uri->rootUrl(), '/');
|
||||
|
||||
return ApiResponse::create([
|
||||
'git_installed' => (bool) Helper::isGitInstalled(),
|
||||
'git_initialized' => (bool) Helper::isGitInitialized(),
|
||||
'configured' => (bool) Helper::isGitSyncConfigured(),
|
||||
'frontend_url' => $frontendUrl,
|
||||
'settings' => [
|
||||
'repository' => (string) ($cfg['repository'] ?? ''),
|
||||
'no_user' => (bool) ($cfg['no_user'] ?? false),
|
||||
'user' => (string) ($cfg['user'] ?? ''),
|
||||
'password_stored' => $password !== '',
|
||||
'branch' => (string) ($cfg['branch'] ?? ''),
|
||||
'webhook' => (string) ($cfg['webhook'] ?? GitSyncPlugin::generateRandomWebhook()),
|
||||
'webhook_enabled' => (bool) ($cfg['webhook_enabled'] ?? false),
|
||||
'webhook_secret' => (string) ($cfg['webhook_secret'] ?? GitSyncPlugin::generateWebhookSecret()),
|
||||
'folders' => array_values((array) ($cfg['folders'] ?? ['pages'])),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the live GitSyncPlugin instance.
|
||||
*
|
||||
* `$grav['plugins']->get('git-sync')` returns a Data wrapper around the
|
||||
* blueprint, not the plugin instance — this fetches the actual plugin
|
||||
* via Plugins::getPlugin(), which is what we need to call synchronize()
|
||||
* and reset().
|
||||
*/
|
||||
private function getGitSyncPlugin(): GitSyncPlugin
|
||||
{
|
||||
$plugin = Plugins::getPlugin('git-sync');
|
||||
if (!$plugin instanceof GitSyncPlugin) {
|
||||
throw new ValidationException('Git Sync plugin is not loaded.');
|
||||
}
|
||||
return $plugin;
|
||||
}
|
||||
|
||||
private function writePluginConfig(array $data): void
|
||||
{
|
||||
$locator = $this->grav['locator'];
|
||||
$pluginsDir = $locator->findResource('config://plugins', true, true);
|
||||
if (!$pluginsDir) {
|
||||
throw new ValidationException('Could not resolve config://plugins directory.');
|
||||
}
|
||||
if (!is_dir($pluginsDir)) {
|
||||
@mkdir($pluginsDir, 0775, true);
|
||||
}
|
||||
|
||||
$file = CompiledYamlFile::instance($pluginsDir . '/git-sync.yaml');
|
||||
$file->save($data);
|
||||
$file->free();
|
||||
|
||||
$this->config->set('plugins.git-sync', $data);
|
||||
|
||||
$cache = $this->grav['cache'] ?? null;
|
||||
if ($cache && method_exists($cache, 'clearCache')) {
|
||||
$cache->clearCache('standard');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,551 @@
|
||||
<?php
|
||||
namespace Grav\Plugin\GitSync;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Plugin;
|
||||
use Grav\Common\Utils;
|
||||
use http\Exception\RuntimeException;
|
||||
use RocketTheme\Toolbox\File\File;
|
||||
use SebastianBergmann\Git\Git;
|
||||
|
||||
class GitSync extends Git
|
||||
{
|
||||
/** @var static */
|
||||
static public $instance;
|
||||
|
||||
/** @var Grav */
|
||||
protected $grav;
|
||||
/** @var Plugin */
|
||||
protected $plugin;
|
||||
/** @var array */
|
||||
protected $config;
|
||||
/** @var string */
|
||||
protected $repositoryPath;
|
||||
|
||||
/** @var string|null */
|
||||
private $user;
|
||||
/** @var string|null */
|
||||
private $password;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->grav = Grav::instance();
|
||||
$this->config = $this->grav['config']->get('plugins.git-sync') ?? [];
|
||||
$this->repositoryPath = isset($this->config['local_repository']) && $this->config['local_repository'] ? $this->config['local_repository'] : USER_DIR;
|
||||
|
||||
parent::__construct($this->repositoryPath);
|
||||
|
||||
static::$instance = $this;
|
||||
|
||||
$this->user = isset($this->config['no_user']) && $this->config['no_user'] ? '' : ($this->config['user'] ?? null);
|
||||
$this->password = $this->config['password'] ?? null;
|
||||
|
||||
unset($this->config['user'], $this->config['password']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function instance()
|
||||
{
|
||||
if (null === static::$instance) {
|
||||
static::$instance = new static;
|
||||
}
|
||||
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getPassword()
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $config
|
||||
*/
|
||||
public function setConfig($config)
|
||||
{
|
||||
$this->config = $config ?? [];
|
||||
$this->user = $this->config['user'] ?? null;
|
||||
$this->password = $this->config['password'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getRuntimeInformation()
|
||||
{
|
||||
$result = [
|
||||
'repositoryPath' => $this->repositoryPath,
|
||||
'username' => $this->user,
|
||||
'password' => $this->password
|
||||
];
|
||||
|
||||
foreach ($this->config as $key => $item) {
|
||||
if (is_array($item)) {
|
||||
$count = count($item);
|
||||
$arr = $item;
|
||||
if ($count === 0) {// empty array, could still be associative
|
||||
$arr = '[]';
|
||||
} else if (isset($item[0])) {// fast check for plain array with numeric keys
|
||||
$arr = '[\'' . implode('\', \'', $item) . '\']';
|
||||
}
|
||||
$result[$key] = $arr;
|
||||
} else {
|
||||
$result[$key] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return string[]
|
||||
*/
|
||||
public function testRepository($url, $branch)
|
||||
{
|
||||
if (!preg_match(Helper::GIT_REGEX, $url)) {
|
||||
throw new \RuntimeException("Git Repository value does not match the supported format.");
|
||||
}
|
||||
|
||||
$branch = $branch ? '"' . $branch . '"' : '';
|
||||
return $this->execute("ls-remote \"{$url}\" {$branch}");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function initializeRepository()
|
||||
{
|
||||
if (!Helper::isGitInitialized()) {
|
||||
$branch = $this->getRemote('branch', null);
|
||||
$local_branch = $this->getConfig('branch', $branch);
|
||||
$this->execute('init');
|
||||
$this->execute('checkout -b ' . $local_branch, true);
|
||||
}
|
||||
|
||||
$this->enableSparseCheckout();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $name
|
||||
* @param string|null $email
|
||||
* @return bool
|
||||
*/
|
||||
public function setUser($name = null, $email = null)
|
||||
{
|
||||
$gitConfig = $this->getConfig('git', []) ?? [];
|
||||
// Fall back to defaults when the config value is missing OR an empty
|
||||
// string — `??` alone leaves a blank name/email in place, which makes
|
||||
// git reject the commit with "fatal: empty ident name ... not allowed".
|
||||
$name = $name ?: (($gitConfig['name'] ?? '') ?: 'GitSync');
|
||||
$email = $email ?: (($gitConfig['email'] ?? '') ?: 'git-sync@trilby.media');
|
||||
$privateKey = $this->getGitConfig('private_key', null);
|
||||
|
||||
$this->execute("config user.name \"{$name}\"");
|
||||
$this->execute("config user.email \"{$email}\"");
|
||||
|
||||
if ($privateKey) {
|
||||
$this->execute('config core.sshCommand "ssh -i ' . $privateKey . ' -F /dev/null"');
|
||||
} else {
|
||||
$this->execute('config --unset core.sshCommand');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $name
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRemote($name = null)
|
||||
{
|
||||
$name = $this->getRemote('name', $name);
|
||||
|
||||
try {
|
||||
/** @var string $version */
|
||||
$version = Helper::isGitInstalled(true);
|
||||
// remote get-url 'name' supported from 2.7.0 and above
|
||||
if (version_compare($version, '2.7.0', '>=')) {
|
||||
$command = "remote get-url \"{$name}\"";
|
||||
} else {
|
||||
$command = "config --get remote.{$name}.url";
|
||||
}
|
||||
|
||||
$this->execute($command);
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function enableSparseCheckout()
|
||||
{
|
||||
$folders = $this->config['folders'] ?? ['pages'];
|
||||
$this->execute('config core.sparsecheckout true');
|
||||
|
||||
$sparse = [];
|
||||
foreach ($folders as $folder) {
|
||||
$sparse[] = $folder . '/';
|
||||
$sparse[] = $folder . '/*';
|
||||
}
|
||||
|
||||
$file = File::instance(rtrim($this->repositoryPath, '/') . '/.git/info/sparse-checkout');
|
||||
$file->save(implode("\r\n", $sparse));
|
||||
$file->free();
|
||||
|
||||
$ignore = ['/*'];
|
||||
foreach ($folders as $folder) {
|
||||
$folder = rtrim($folder,'/');
|
||||
$nested = substr_count($folder, '/');
|
||||
|
||||
if ($nested) {
|
||||
$subfolders = explode('/', $folder);
|
||||
$nested_tracking = '';
|
||||
foreach ($subfolders as $index => $subfolder) {
|
||||
$last = $index === (count($subfolders) - 1);
|
||||
$nested_tracking .= $subfolder . '/';
|
||||
if (!in_array('!/' . $nested_tracking, $ignore, true)) {
|
||||
$ignore[] = rtrim($nested_tracking . (!$last ? '*' : ''), '/');
|
||||
$ignore[] = rtrim('!/' . $nested_tracking, '/');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$ignore[] = '!/' . $folder;
|
||||
}
|
||||
}
|
||||
|
||||
$ignoreEntries = explode("\n", $this->getGitConfig('ignore', ''));
|
||||
$ignore = array_merge($ignore, $ignoreEntries);
|
||||
|
||||
$file = File::instance(rtrim($this->repositoryPath, '/') . '/.gitignore');
|
||||
$file->save(implode("\r\n", $ignore));
|
||||
$file->free();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $alias
|
||||
* @param string|null $url
|
||||
* @param bool $authenticated
|
||||
* @return string[]
|
||||
*/
|
||||
public function addRemote($alias = null, $url = null, $authenticated = false)
|
||||
{
|
||||
$alias = $this->getRemote('name', $alias);
|
||||
$url = $this->getConfig('repository', $url);
|
||||
|
||||
if ($authenticated) {
|
||||
$user = $this->user ?? '';
|
||||
$password = $this->password ? Helper::decrypt($this->password) : '';
|
||||
$url = Helper::prepareRepository($user, $password, $url);
|
||||
}
|
||||
|
||||
$command = $this->hasRemote($alias) ? 'set-url' : 'add';
|
||||
|
||||
return $this->execute("remote {$command} {$alias} \"{$url}\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function add()
|
||||
{
|
||||
/** @var string $version */
|
||||
$version = Helper::isGitInstalled(true);
|
||||
$add = 'add';
|
||||
|
||||
// With the introduction of customizable paths,
|
||||
// it appears that the add command should always
|
||||
// add everything that is not committed to ensure
|
||||
// there are no orphan changes left behind
|
||||
|
||||
/*
|
||||
$folders = $this->config['folders'] ?? ['pages'];
|
||||
$paths = [];
|
||||
foreach ($folders as $folder) {
|
||||
$paths[] = $folder;
|
||||
}
|
||||
*/
|
||||
|
||||
$paths = ['.'];
|
||||
|
||||
if (version_compare($version, '2.0', '<')) {
|
||||
$add .= ' --all';
|
||||
}
|
||||
|
||||
return $this->execute($add . ' ' . implode(' ', $paths));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
* @return string[]
|
||||
*/
|
||||
public function commit($message = '(Grav GitSync) Automatic Commit')
|
||||
{
|
||||
$authorType = $this->getGitConfig('author', 'gituser');
|
||||
if (defined('GRAV_CLI') && in_array($authorType, ['gravuser', 'gravfull'])) {
|
||||
$authorType = 'gituser';
|
||||
}
|
||||
|
||||
// get message from config, it any, or stick to the default one
|
||||
$config = $this->getConfig('git', null);
|
||||
$message = $config['message'] ?? $message;
|
||||
|
||||
// get Page Title and Route from Post
|
||||
$uri = $this->grav['uri'];
|
||||
$page_title = $uri->post('data.header.title');
|
||||
$page_route = $uri->post('data.route');
|
||||
|
||||
$pageTitle = $page_title ?: 'NO TITLE FOUND';
|
||||
$pageRoute = $page_route ?: 'NO ROUTE FOUND';
|
||||
|
||||
// include page title and route in the message, if placeholders exist
|
||||
$message = str_replace('{{pageTitle}}', $pageTitle, $message);
|
||||
/** @var string $message */
|
||||
$message = str_replace('{{pageRoute}}', $pageRoute, $message);
|
||||
|
||||
$gitConfig = $this->getConfig('git', []) ?? [];
|
||||
switch ($authorType) {
|
||||
case 'gitsync':
|
||||
$user = $gitConfig['name'] ?? 'GitSync';
|
||||
$email = $gitConfig['email'] ?? 'git-sync@trilby.media';
|
||||
break;
|
||||
case 'gravuser':
|
||||
$user = $this->grav['session']->user->username ?? 'GitSync';
|
||||
$email = $this->grav['session']->user->email ?? 'git-sync@trilby.media';
|
||||
break;
|
||||
case 'gravfull':
|
||||
$user = $this->grav['session']->user->fullname ?? 'GitSync';
|
||||
$email = $this->grav['session']->user->email ?? 'git-sync@trilby.media';
|
||||
break;
|
||||
case 'gituser':
|
||||
default:
|
||||
$user = $this->user ?? 'GitSync';
|
||||
$email = $gitConfig['email'] ?? 'git-sync@trilby.media';
|
||||
break;
|
||||
}
|
||||
|
||||
// Guard against empty values from any source (e.g. a Grav user with no
|
||||
// full name set, or a blank committer field) — an empty author name
|
||||
// triggers git's "fatal: empty ident name ... not allowed".
|
||||
$user = $user ?: 'GitSync';
|
||||
$email = $email ?: 'git-sync@trilby.media';
|
||||
|
||||
$author = $user . ' <' . $email . '>';
|
||||
$author = '--author="' . $author . '"';
|
||||
$message .= ' from ' . $user;
|
||||
$this->add();
|
||||
|
||||
return $this->execute('commit ' . $author . ' -m ' . escapeshellarg($message));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $name
|
||||
* @param string|null $branch
|
||||
* @return string[]
|
||||
*/
|
||||
public function fetch($name = null, $branch = null)
|
||||
{
|
||||
$name = $this->getRemote('name', $name);
|
||||
$branch = $this->getRemote('branch', $branch);
|
||||
|
||||
return $this->execute("fetch {$name} {$branch}");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $name
|
||||
* @param string|null $branch
|
||||
* @return string[]
|
||||
*/
|
||||
public function pull($name = null, $branch = null)
|
||||
{
|
||||
$name = $this->getRemote('name', $name);
|
||||
$branch = $this->getRemote('branch', $branch);
|
||||
/** @var string $version */
|
||||
$version = Helper::isGitInstalled(true);
|
||||
$unrelated_histories = '--allow-unrelated-histories';
|
||||
|
||||
// --allow-unrelated-histories starts at 2.9.0
|
||||
if (version_compare($version, '2.9.0', '<')) {
|
||||
$unrelated_histories = '';
|
||||
}
|
||||
|
||||
return $this->execute("pull {$unrelated_histories} --ff -X theirs {$name} {$branch}");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $name
|
||||
* @param string|null $branch
|
||||
* @return string[]
|
||||
*/
|
||||
public function push($name = null, $branch = null)
|
||||
{
|
||||
$name = $this->getRemote('name', $name);
|
||||
$branch = $this->getRemote('branch', $branch);
|
||||
$local_branch = $this->getConfig('branch', null);
|
||||
|
||||
return $this->execute("push {$name} {$local_branch}:{$branch}");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $name
|
||||
* @param string|null $branch
|
||||
* @return bool
|
||||
*/
|
||||
public function sync($name = null, $branch = null)
|
||||
{
|
||||
$name = $this->getRemote('name', $name);
|
||||
$branch = $this->getRemote('branch', $branch);
|
||||
$this->addRemote(null, null, true);
|
||||
|
||||
$this->fetch($name, $branch);
|
||||
$this->pull($name, $branch);
|
||||
if ($this->grav['config']->get('plugins.git-sync.sync.direction', 'both') == 'both') {
|
||||
$this->push($name, $branch);
|
||||
}
|
||||
|
||||
$this->addRemote();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
return $this->execute('reset --hard HEAD');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isWorkingCopyClean()
|
||||
{
|
||||
$message = 'nothing to commit';
|
||||
$output = $this->execute('status');
|
||||
|
||||
return strpos($output[count($output) - 1], $message) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasChangesToCommit()
|
||||
{
|
||||
$folders = $this->config['folders'] ?? ['pages'];
|
||||
$paths = [];
|
||||
|
||||
foreach ($folders as $folder) {
|
||||
$folder = explode('/', $folder);
|
||||
$paths[] = array_shift($folder);
|
||||
}
|
||||
|
||||
$message = 'nothing to commit';
|
||||
$output = $this->execute('status ' . implode(' ', $paths));
|
||||
|
||||
return strpos($output[count($output) - 1], $message) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $command
|
||||
* @param bool $quiet
|
||||
* @return string[]
|
||||
*/
|
||||
public function execute($command, $quiet = false)
|
||||
{
|
||||
try {
|
||||
$bin = Helper::getGitBinary($this->getGitConfig('bin', 'git'));
|
||||
/** @var string $version */
|
||||
$version = Helper::isGitInstalled(true);
|
||||
|
||||
// -C <path> supported from 1.8.5 and above
|
||||
if (version_compare($version, '1.8.5', '>=')) {
|
||||
$command = $bin . ' -C ' . escapeshellarg($this->repositoryPath) . ' ' . $command;
|
||||
} else {
|
||||
$command = 'cd ' . $this->repositoryPath . ' && ' . $bin . ' ' . $command;
|
||||
}
|
||||
|
||||
$command .= ' 2>&1';
|
||||
|
||||
if (DIRECTORY_SEPARATOR === '/') {
|
||||
$command = 'LC_ALL=C ' . $command;
|
||||
}
|
||||
|
||||
if ($this->getConfig('logging', false)) {
|
||||
$log_command = Helper::preventReadablePassword($command, $this->password ?? '');
|
||||
$this->grav['log']->notice('gitsync[command]: ' . $log_command);
|
||||
|
||||
exec($command, $output, $returnValue);
|
||||
|
||||
$log_output = Helper::preventReadablePassword(implode("\n", $output), $this->password ?? '');
|
||||
$this->grav['log']->notice('gitsync[output]: ' . $log_output);
|
||||
} else {
|
||||
exec($command, $output, $returnValue);
|
||||
}
|
||||
|
||||
if ($returnValue !== 0 && $returnValue !== 5 && !$quiet) {
|
||||
throw new \RuntimeException(implode("\r\n", $output));
|
||||
}
|
||||
|
||||
return $output;
|
||||
} catch (\RuntimeException $e) {
|
||||
$message = $e->getMessage();
|
||||
$message = Helper::preventReadablePassword($message, $this->password ?? '');
|
||||
|
||||
// handle scary messages
|
||||
if (Utils::contains($message, 'remote: error: cannot lock ref')) {
|
||||
$message = 'GitSync: An error occurred while trying to synchronize. This could mean GitSync is already running. Please try again.';
|
||||
}
|
||||
|
||||
throw new \RuntimeException($message);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
public function getGitConfig($type, $value)
|
||||
{
|
||||
return $this->config['git'][$type] ?? $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRemote($type, $value)
|
||||
{
|
||||
return $value ?: ($this->config['remote'][$type] ?? $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
public function getConfig($type, $value)
|
||||
{
|
||||
return $value ?: ($this->config[$type] ?? $value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
namespace Grav\Plugin\GitSync;
|
||||
|
||||
use Defuse\Crypto\Crypto;
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Utils;
|
||||
use SebastianBergmann\Git\RuntimeException;
|
||||
|
||||
class Helper
|
||||
{
|
||||
/** @var string */
|
||||
private static $hash = '594ef69d-6c29-45f7-893a-f1b4342687d3';
|
||||
|
||||
/** @var string */
|
||||
const GIT_REGEX = '/(?:git|ssh|https?|git@[-\w.]+):(\/\/)?(.*?)(\.git)(\/?|\#[-\d\w._]+?)$/';
|
||||
|
||||
/**
|
||||
* Checks if git-sync is properly configured with a repository URL
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isGitSyncConfigured()
|
||||
{
|
||||
$config = Grav::instance()['config']->get('plugins.git-sync');
|
||||
$repository = $config['repository'] ?? null;
|
||||
return !empty($repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if git-sync is ready to use (installed, configured, and initialized)
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isGitSyncReady()
|
||||
{
|
||||
return static::isGitInstalled() && static::isGitSyncConfigured() && static::isGitInitialized();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user/ folder is already initialized
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isGitInitialized()
|
||||
{
|
||||
/** @var Config $grav */
|
||||
$config = Grav::instance()['config']->get('plugins.git-sync');
|
||||
$repositoryPath = isset($config['local_repository']) && $config['local_repository'] ? $config['local_repository'] : USER_DIR;
|
||||
return file_exists(rtrim($repositoryPath, '/') . '/.git');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $version
|
||||
* @return bool|string
|
||||
*/
|
||||
public static function isGitInstalled($version = false)
|
||||
{
|
||||
$bin = Helper::getGitBinary();
|
||||
|
||||
exec($bin . ' --version', $output, $returnValue);
|
||||
|
||||
$installed = $returnValue === 0;
|
||||
|
||||
if ($version && $output) {
|
||||
$output = explode(' ', array_shift($output));
|
||||
$versions = array_filter($output, static function($item) {
|
||||
return version_compare($item, '0.0.1', '>=');
|
||||
});
|
||||
|
||||
$installed = array_shift($versions);
|
||||
}
|
||||
|
||||
return $installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $override
|
||||
* @return string
|
||||
*/
|
||||
public static function getGitBinary($override = false)
|
||||
{
|
||||
/** @var Config $grav */
|
||||
$config = Grav::instance()['config'];
|
||||
|
||||
return $override ?: $config->get('plugins.git-sync.git.bin', 'git');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $user
|
||||
* @param string $password
|
||||
* @param string $repository
|
||||
* @return string
|
||||
*/
|
||||
public static function prepareRepository($user, $password, $repository)
|
||||
{
|
||||
$user = $user ? urlencode($user) . ':' : '';
|
||||
$password = urlencode($password);
|
||||
|
||||
if (Utils::startsWith($repository, 'ssh://')) {
|
||||
return $repository;
|
||||
}
|
||||
|
||||
return str_replace('://', "://{$user}{$password}@", $repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $user
|
||||
* @param string $password
|
||||
* @param string $repository
|
||||
* @return string[]
|
||||
*/
|
||||
public static function testRepository($user, $password, $repository, $branch)
|
||||
{
|
||||
$git = new GitSync();
|
||||
$repository = self::prepareRepository($user, $password, $repository);
|
||||
|
||||
try {
|
||||
return $git->testRepository($repository, $branch);
|
||||
} catch (RuntimeException $e) {
|
||||
return [$e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $password
|
||||
* @return string
|
||||
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
|
||||
*/
|
||||
public static function encrypt($password)
|
||||
{
|
||||
return 'gitsync-' . Crypto::encryptWithPassword($password, self::$hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $enc_password
|
||||
* @return string
|
||||
*/
|
||||
public static function decrypt($enc_password)
|
||||
{
|
||||
if (strpos($enc_password, 'gitsync-') === 0) {
|
||||
$enc_password = substr($enc_password, 8);
|
||||
|
||||
return Crypto::decryptWithPassword($enc_password, self::$hash);
|
||||
}
|
||||
|
||||
return $enc_password;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public static function synchronize()
|
||||
{
|
||||
if (!self::isGitInstalled() || !self::isGitInitialized()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$git = new GitSync();
|
||||
|
||||
if ($git->hasChangesToCommit()) {
|
||||
$git->commit();
|
||||
}
|
||||
|
||||
// synchronize with remote
|
||||
$git->sync();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $str
|
||||
* @param string $password
|
||||
* @return string
|
||||
*/
|
||||
public static function preventReadablePassword($str, $password)
|
||||
{
|
||||
$encoded = urlencode(self::decrypt($password));
|
||||
|
||||
return str_replace($encoded, '{password}', $str);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user