Add REST API endpoints for repo folder listing and page pushing

- Implemented `GET /repo-folders` to list available sub-folders in the configured repository.
- Added `POST /push` to push a single page to the repository.
- Introduced `POST /push-all` to push all synced pages back to the repository.
- Enhanced `oribi_sync_rest_sync` to push local changes after pulling, except during dry runs.
- Created `oribi_sync_push_page` and `oribi_sync_push_all` functions to handle page pushing logic.
- Updated post meta on successful pushes to track last push time and SHA.
- Added logging for push actions and errors.

Enhance sync engine to support theme file synchronization

- Added functionality to auto-apply changed theme files from the repository's theme directory.
- Created `oribi_sync_apply_theme_files` to handle theme file updates during sync.
- Ensured the existence of a minimal theme structure in the `ots-theme` directory.

Refactor uninstall process to clean up additional post meta

- Updated `uninstall.php` to remove new post meta related to push operations.
- Ensured comprehensive cleanup of options and metadata upon plugin uninstallation.

Introduce push client for handling page pushes to Gitea

- Created `push-client.php` to encapsulate logic for pushing pages back to the Git repository.
- Implemented conflict resolution by creating branches and opening pull requests when necessary.
- Added helper functions for authenticated API requests to Gitea.
This commit is contained in:
Matt Batchelder
2026-02-20 21:03:48 -05:00
parent f528f21573
commit d2228ed0fb
9 changed files with 1144 additions and 115 deletions

View File

@@ -90,12 +90,13 @@ function oribi_sync_execute_php( string $php_source, string $slug ) {
*/
function oribi_sync_run( bool $dry_run = false ): array {
$result = [
'ok' => true,
'created' => [],
'updated' => [],
'trashed' => [],
'skipped' => [],
'errors' => [],
'ok' => true,
'created' => [],
'updated' => [],
'trashed' => [],
'skipped' => [],
'errors' => [],
'theme_updated' => [],
];
// ── Gather settings ────────────────────────────────────────────────────
@@ -128,18 +129,21 @@ function oribi_sync_run( bool $dry_run = false ): array {
return $result;
}
// ── Filter to pages/ directory ─────────────────────────────────────────
$page_files = oribi_sync_filter_tree( $tree, 'pages' );
// ── Filter to selected Pages sub-folder ────────────────────────────────
$pages_folder = get_option( 'oribi_sync_pages_folder', '' );
$synced_slugs = [];
if ( empty( $page_files ) ) {
$result['errors'][] = 'No files found under pages/ in the repository.';
$result['ok'] = false;
return $result;
if ( empty( $pages_folder ) ) {
$result['skipped'][] = 'Pages sync skipped — no folder selected in settings.';
$page_files = [];
} else {
$page_files = oribi_sync_filter_tree( $tree, 'Pages/' . $pages_folder );
if ( empty( $page_files ) ) {
$result['errors'][] = 'No files found under Pages/' . $pages_folder . '/ in the repository.';
}
}
// ── Process each page file ─────────────────────────────────────────────
$synced_slugs = [];
foreach ( $page_files as $entry ) {
$filename = basename( $entry['path'] );
$slug = oribi_sync_filename_to_slug( $filename );
@@ -239,7 +243,8 @@ function oribi_sync_run( bool $dry_run = false ): array {
update_post_meta( $existing->ID, '_oribi_sync_last_run', current_time( 'mysql' ) );
update_post_meta( $existing->ID, '_wp_page_template', 'default' );
$result['updated'][] = $slug;
$content_size = strlen( $content );
$result['updated'][] = $slug . ' (' . $content_size . ' bytes)';
} else {
// Create new page
$title = oribi_sync_slug_to_title( $slug );
@@ -263,16 +268,25 @@ function oribi_sync_run( bool $dry_run = false ): array {
update_post_meta( $post_id, '_oribi_sync_last_run', current_time( 'mysql' ) );
update_post_meta( $post_id, '_wp_page_template', 'default' );
$result['created'][] = $slug;
$content_size = strlen( $content );
$result['created'][] = $slug . ' (' . $content_size . ' bytes)';
}
}
// ── Trash pages removed from repo ──────────────────────────────────────
if ( ! $dry_run ) {
if ( ! $dry_run && ! empty( $pages_folder ) ) {
$trashed = oribi_sync_trash_removed_pages( $synced_slugs );
$result['trashed'] = $trashed;
}
// ── Auto-sync theme files ──────────────────────────────────────────────
// Reuse the already-fetched $tree so we don't make a second API call.
$theme_sync = oribi_sync_apply_theme_files( $api_base, $branch, $provider, $pat, $dry_run, $tree );
$result['theme_updated'] = $theme_sync['updated'];
foreach ( $theme_sync['errors'] as $err ) {
$result['errors'][] = '[theme] ' . $err;
}
// ── Record run ─────────────────────────────────────────────────────────
if ( ! $dry_run ) {
oribi_sync_record_run( $result );
@@ -357,12 +371,13 @@ function oribi_sync_record_run( array $result ): void {
if ( ! is_array( $log ) ) $log = [];
array_unshift( $log, [
'time' => current_time( 'mysql' ),
'created' => $result['created'],
'updated' => $result['updated'],
'trashed' => $result['trashed'],
'skipped' => $result['skipped'],
'errors' => $result['errors'],
'time' => current_time( 'mysql' ),
'created' => $result['created'],
'updated' => $result['updated'],
'trashed' => $result['trashed'],
'skipped' => $result['skipped'],
'errors' => $result['errors'],
'theme_updated' => $result['theme_updated'] ?? [],
] );
// Keep last 20 entries
@@ -370,6 +385,125 @@ function oribi_sync_record_run( array $result ): void {
update_option( 'oribi_sync_log', $log, 'no' );
}
/**
* Ensure the ots-theme directory exists with a minimal style.css header.
*
* WordPress requires style.css with a "Theme Name:" header to recognise a theme.
* If the repo's theme/ folder already contains a style.css it will overwrite
* this stub during sync, so the stub is only a bootstrap.
*/
function oribi_sync_ensure_ots_theme( string $theme_dir ): void {
if ( is_dir( $theme_dir ) && file_exists( $theme_dir . '/style.css' ) ) {
return; // Already exists.
}
if ( ! is_dir( $theme_dir ) ) {
wp_mkdir_p( $theme_dir );
}
// Only write the stub if style.css doesn't exist yet.
if ( ! file_exists( $theme_dir . '/style.css' ) ) {
$header = <<<'CSS'
/*
Theme Name: OTS Theme
Description: Auto-created by Oribi Tech Sync. Theme files are managed via Git.
Version: 1.0.0
Author: Oribi Technology Services
*/
CSS;
file_put_contents( $theme_dir . '/style.css', $header );
}
// Create a minimal index.php (required by WP theme standards).
if ( ! file_exists( $theme_dir . '/index.php' ) ) {
file_put_contents( $theme_dir . '/index.php', "<?php\n// Silence is golden.\n" );
}
}
/**
* Auto-apply changed theme files from the repo's theme/ directory to ots-theme.
*
* Called as part of every sync run.
*
* @param string $api_base Provider API base URL.
* @param string $branch Branch name.
* @param string $provider Provider key.
* @param string $pat Personal access token.
* @param bool $dry_run If true, report changes without writing files.
*
* @return array{updated: string[], errors: string[]}
*/
function oribi_sync_apply_theme_files( string $api_base, string $branch, string $provider, string $pat, bool $dry_run = false, ?array $tree = null ): array {
$out = [ 'updated' => [], 'errors' => [] ];
$allowed = [ 'css', 'js', 'json', 'php', 'html', 'htm', 'svg', 'txt' ];
$theme_dir = get_theme_root() . '/ots-theme';
// Create the ots-theme if it does not exist yet.
if ( ! $dry_run ) {
oribi_sync_ensure_ots_theme( $theme_dir );
}
// Fetch the tree only if one wasn't passed in (avoids a redundant API call during sync).
if ( $tree === null ) {
$tree = oribi_sync_fetch_tree( $api_base, $branch, $provider, $pat );
if ( is_wp_error( $tree ) ) {
$out['errors'][] = 'Tree fetch failed: ' . $tree->get_error_message();
return $out;
}
}
$theme_entries = oribi_sync_filter_tree( $tree, 'theme', true );
foreach ( $theme_entries as $entry ) {
$relative = substr( $entry['path'], strlen( 'theme/' ) );
$ext = strtolower( pathinfo( $relative, PATHINFO_EXTENSION ) );
if ( ! in_array( $ext, $allowed, true ) ) {
continue;
}
// Fetch content from repo
$content = oribi_sync_fetch_file( $api_base, $branch, $entry['path'], $provider, $pat );
if ( is_wp_error( $content ) ) {
$out['errors'][] = $entry['path'] . ': ' . $content->get_error_message();
continue;
}
$dest = $theme_dir . '/' . $relative;
$local_exists = file_exists( $dest );
$local_content = $local_exists ? file_get_contents( $dest ) : null;
// Skip unchanged files
if ( $local_exists && $local_content === $content ) {
continue;
}
if ( $dry_run ) {
$out['updated'][] = $relative;
continue;
}
// Create subdirectory if needed
$dir = dirname( $dest );
if ( ! is_dir( $dir ) ) {
if ( ! wp_mkdir_p( $dir ) ) {
$out['errors'][] = $relative . ' — could not create directory.';
continue;
}
}
$written = file_put_contents( $dest, $content );
if ( $written === false ) {
$out['errors'][] = $relative . ' — write failed (check permissions).';
} else {
$out['updated'][] = $relative;
}
}
return $out;
}
/**
* Fetch theme files from the repo (for preview / apply).
*
@@ -414,9 +548,10 @@ function oribi_sync_fetch_theme_files(): array {
continue;
}
// Check if a matching file exists in the active theme
$theme_file = get_template_directory() . '/' . $relative;
$local_exists = file_exists( $theme_file );
// Check if a matching file exists in the ots-theme
$theme_dir = get_theme_root() . '/ots-theme';
$theme_file = $theme_dir . '/' . $relative;
$local_exists = file_exists( $theme_file );
$local_content = $local_exists ? file_get_contents( $theme_file ) : null;
$out['files'][] = [