Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/Composer/RootComposerJsonFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,65 @@ public function writeFile(string $path)
{
file_put_contents($path, $this->render());
}

/**
* Get the raw content object
*/
public function getContent(): stdClass
{
return $this->content;
}

/**
* Deep-merge sections from a source composer.json into this one.
*
* Adds missing keys from source without overwriting existing entries.
* Used to ensure bundle baseline deps are present while preserving
* user-added dependencies.
*/
public function mergeFrom(self $source): self
{
$sourceContent = $source->getContent();

foreach (['require', 'require-dev', 'suggest'] as $section) {
if (!isset($sourceContent->$section)) {
continue;
}
if (!isset($this->content->$section)) {
$this->content->$section = new stdClass();
}
foreach ($sourceContent->$section as $package => $constraint) {
if (!isset($this->content->$section->$package)) {
$this->content->$section->$package = $constraint;
}
}
}

if (isset($sourceContent->config->{'allow-plugins'})) {
if (!isset($this->content->config)) {
$this->content->config = new stdClass();
}
if (!isset($this->content->config->{'allow-plugins'})) {
$this->content->config->{'allow-plugins'} = new stdClass();
}
foreach ($sourceContent->config->{'allow-plugins'} as $plugin => $allowed) {
if (!isset($this->content->config->{'allow-plugins'}->$plugin)) {
$this->content->config->{'allow-plugins'}->$plugin = $allowed;
}
}
}

if (isset($sourceContent->extra->{'installer-types'})) {
if (!isset($this->content->extra)) {
$this->content->extra = new stdClass();
}
$existing = (array) ($this->content->extra->{'installer-types'} ?? []);
$sourceTypes = (array) $sourceContent->extra->{'installer-types'};
$this->content->extra->{'installer-types'} = array_values(
array_unique(array_merge($existing, $sourceTypes))
);
}

return $this;
}
}
13 changes: 9 additions & 4 deletions src/Helper/Composer.php
Original file line number Diff line number Diff line change
Expand Up @@ -485,8 +485,9 @@ protected function _setAuthors(WrapperHordeYml $package, stdClass $composerDefin
*/
protected function _setAutoload(WrapperHordeYml $package, stdClass $composerDefinition): void
{
// Extensions don't need PHP autoloading (they're compiled C code)
if ($package['type'] === 'extension') {
// Extensions and themes don't need PHP autoloading
if (in_array($package['type'], ['extension', 'horde-theme'], true)) {
$composerDefinition->autoload = new stdClass();
return;
}

Expand Down Expand Up @@ -523,6 +524,9 @@ protected function _setAutoload(WrapperHordeYml $package, stdClass $composerDefi
$composerDefinition->autoload['psr-4'] = [$Psr4Name => 'src/'];
}
}
if (empty($composerDefinition->autoload)) {
$composerDefinition->autoload = new stdClass();
}
}
/**
* Configure Autoloading
Expand All @@ -534,8 +538,9 @@ protected function _setAutoload(WrapperHordeYml $package, stdClass $composerDefi
*/
protected function _setAutoloadDev(WrapperHordeYml $package, stdClass $composerDefinition): void
{
// Extensions don't need PHP autoloading (they're compiled C code)
if ($package['type'] === 'extension') {
// Extensions and themes don't need PHP autoloading
if (in_array($package['type'], ['extension', 'horde-theme'], true)) {
$composerDefinition->{'autoload-dev'} = new stdClass();
return;
}

Expand Down
51 changes: 47 additions & 4 deletions src/Qc/Task/Gitignore.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

namespace Horde\Components\Qc\Task;

use Horde\HordeYmlFile\HordeYmlFile;

/**
* Components_Qc_Task_Gitignore:: checks .gitignore for required entries.
*
Expand Down Expand Up @@ -44,6 +46,19 @@ class Gitignore extends Base
'/.phpstan.cache/' => 'PHPStan cache directory',
];

/**
* Additional entries for components using the horde-installer-plugin
*/
private const INSTALLER_PLUGIN_ENTRIES = [
'/var/' => 'Horde installer plugin runtime data',
'/web/' => 'Horde installer plugin web-accessible directory',
];

/**
* Component types that use the horde-installer-plugin
*/
private const INSTALLER_PLUGIN_TYPES = ['library', 'application', 'component', 'horde-theme'];

/**
* Get the name of this task.
*
Expand All @@ -69,13 +84,14 @@ public function run(array &$options = []): int
$componentPath = getcwd();
}

$requiredEntries = $this->getRequiredEntries($componentPath);
$gitignorePath = $this->findGitignore($componentPath);

if ($gitignorePath === null) {
$this->getOutput()->warn('No .gitignore file found');

if (!empty($options['fix_qc_issues'])) {
return $this->createGitignore($componentPath);
return $this->createGitignore($componentPath, $requiredEntries);
}

return 1; // Error: no .gitignore found
Expand All @@ -89,7 +105,7 @@ public function run(array &$options = []): int

// Check for required entries
$missing = [];
foreach (self::REQUIRED_ENTRIES as $pattern => $description) {
foreach ($requiredEntries as $pattern => $description) {
if (!$this->hasPattern($lines, $pattern)) {
$missing[$pattern] = $description;
}
Expand Down Expand Up @@ -180,17 +196,18 @@ private function hasPattern(array $lines, string $pattern): bool
* Create a new .gitignore file with required entries.
*
* @param string $componentPath Path to the component.
* @param array $entries Required patterns and descriptions.
*
* @return int Number of errors (0 = success).
*/
private function createGitignore(string $componentPath): int
private function createGitignore(string $componentPath, array $entries): int
{
$gitignorePath = $componentPath . '/.gitignore';

$content = "# Generated by horde-components QC\n";
$content .= "# Standard ignores for Horde components\n\n";

foreach (self::REQUIRED_ENTRIES as $pattern => $description) {
foreach ($entries as $pattern => $description) {
$content .= "# $description\n";
$content .= "$pattern\n\n";
}
Expand Down Expand Up @@ -236,4 +253,30 @@ private function addMissingEntries(string $gitignorePath, array $missing): int
$this->getOutput()->ok('Added ' . count($missing) . ' missing entries to .gitignore');
return 0;
}

/**
* Get the full set of required entries for this component.
*
* Includes installer-plugin entries (var/, web/) when the component
* type uses the horde-installer-plugin.
*
* @param string $componentPath Path to the component.
*
* @return array Pattern => description pairs.
*/
private function getRequiredEntries(string $componentPath): array
{
$entries = self::REQUIRED_ENTRIES;

$hordeYmlPath = $componentPath . '/.horde.yml';
if (file_exists($hordeYmlPath)) {
$hordeYml = new HordeYmlFile($hordeYmlPath);
$type = $hordeYml->getType();
if (in_array($type, self::INSTALLER_PLUGIN_TYPES, true)) {
$entries = array_merge($entries, self::INSTALLER_PLUGIN_ENTRIES);
}
}

return $entries;
}
}
18 changes: 18 additions & 0 deletions src/Runner/InstallRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Horde\Components\Composer\InstallationDirectory;
use Horde\Components\Composer\PathRepositoryDefinition;
use Horde\Components\Composer\RootComposerJsonFile;
use Horde\Components\RuntimeContext\GitCheckoutDirectory;
use Horde\Components\Output;
use Horde\Components\Wrapper\HordeYml;
Expand Down Expand Up @@ -79,16 +80,33 @@ public function run()
filter: [
'vendor',
'composer.lock',
'composer.json',
],
);
$copyHelper->copy();

// Handle composer.json individually for idempotency
if (!$this->installationDirectory->hasComposerJson()) {
copy(
$baseComponentGitDir . '/composer.json',
$this->installationDirectory->getComposerJsonPath()
);
$this->output->ok('Copied composer.json from bundle');
} else {
$this->output->info('Existing composer.json found — merging bundle dependencies');
}

// Inject all horde apps as local sources.
try {
$composerJson = $this->installationDirectory->getComposerJson();
} catch (Exception $e) {
$this->output->fail('Could not read composer.json file from installation directory: ' . $this->installationDirectory);
return;
}

// Merge bundle's baseline sections into existing file (noop for fresh installs)
$bundleJson = RootComposerJsonFile::loadFile($baseComponentGitDir . '/composer.json');
$composerJson->mergeFrom($bundleJson);
foreach ($this->gitCheckoutDirectory->getHordeYmlDirs() as $hordeYmlDir) {
// Load HordeYml to get the ComponentVersion
$hordeYml = new HordeYml($hordeYmlDir);
Expand Down
21 changes: 21 additions & 0 deletions src/Task/Release/AddChangelogEntryTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,31 @@ public function __construct(
}


/**
* Component types that do not use doc/changelog.yml.
*/
private const TYPES_WITHOUT_CHANGELOG = [
'horde-theme',
];

public function getName(): string
{
return "Ad\1 \2hangelo\1 \2ntry";
}

public function shouldSkip(Context $context): bool
{
$componentPath = $context->getComponentPath();
$hordeYmlPath = $componentPath . '/.horde.yml';
if (file_exists($hordeYmlPath)) {
$hordeYml = new HordeYmlFile($hordeYmlPath);
if (in_array($hordeYml->getType(), self::TYPES_WITHOUT_CHANGELOG, true)) {
return true;
}
}
return false;
}

public function run(Context $context): Result
{
$componentPath = $context->getComponentPath();
Expand Down
Loading