{"id":220400,"date":"2026-03-24T22:48:00","date_gmt":"2026-03-24T22:48:00","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/pushpull\/"},"modified":"2026-05-12T21:01:32","modified_gmt":"2026-05-12T21:01:32","slug":"pushpull","status":"publish","type":"plugin","link":"https:\/\/ga.wordpress.org\/plugins\/pushpull\/","author":23149214,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_crdt_document":"","version":"0.0.24","stable_tag":"0.0.24","tested":"6.9.4","requires":"6.0","requires_php":"8.1","requires_plugins":null,"header_name":"PushPull","header_author":"CreativeMoods","header_description":"Push Pull DevOps plugin for Wordpress","assets_banners_color":"f88b1b","last_updated":"2026-05-12 21:01:32","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/github.com\/creativemoods\/pushpull","header_author_uri":"","rating":0,"author_block_rating":0,"active_installs":0,"downloads":666,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","changelog"],"tags":{"0.0.10":{"tag":"0.0.10","author":"jeromesteunenberg","date":"2026-03-28 06:56:09"},"0.0.11":{"tag":"0.0.11","author":"jeromesteunenberg","date":"2026-03-28 08:58:33"},"0.0.12":{"tag":"0.0.12","author":"jeromesteunenberg","date":"2026-03-28 20:48:42"},"0.0.13":{"tag":"0.0.13","author":"jeromesteunenberg","date":"2026-03-29 09:39:27"},"0.0.14":{"tag":"0.0.14","author":"jeromesteunenberg","date":"2026-03-29 15:04:09"},"0.0.15":{"tag":"0.0.15","author":"jeromesteunenberg","date":"2026-03-29 19:35:53"},"0.0.16":{"tag":"0.0.16","author":"jeromesteunenberg","date":"2026-04-06 09:10:49"},"0.0.17":{"tag":"0.0.17","author":"jeromesteunenberg","date":"2026-04-10 11:30:49"},"0.0.18":{"tag":"0.0.18","author":"jeromesteunenberg","date":"2026-04-10 13:46:59"},"0.0.19":{"tag":"0.0.19","author":"jeromesteunenberg","date":"2026-04-11 10:04:46"},"0.0.20":{"tag":"0.0.20","author":"jeromesteunenberg","date":"2026-04-14 10:49:41"},"0.0.21":{"tag":"0.0.21","author":"jeromesteunenberg","date":"2026-05-07 15:29:59"},"0.0.22":{"tag":"0.0.22","author":"jeromesteunenberg","date":"2026-05-12 13:09:39"},"0.0.23":{"tag":"0.0.23","author":"jeromesteunenberg","date":"2026-05-12 14:58:45"},"0.0.24":{"tag":"0.0.24","author":"jeromesteunenberg","date":"2026-05-12 21:01:32"},"0.0.3":{"tag":"0.0.3","author":"jeromesteunenberg","date":"2026-03-24 22:47:37"},"0.0.4":{"tag":"0.0.4","author":"jeromesteunenberg","date":"2026-03-25 13:06:13"},"0.0.5":{"tag":"0.0.5","author":"jeromesteunenberg","date":"2026-03-26 10:43:37"},"0.0.6":{"tag":"0.0.6","author":"jeromesteunenberg","date":"2026-03-26 13:23:52"},"0.0.7":{"tag":"0.0.7","author":"jeromesteunenberg","date":"2026-03-26 14:55:53"},"0.0.8":{"tag":"0.0.8","author":"jeromesteunenberg","date":"2026-03-27 14:19:19"},"0.0.9":{"tag":"0.0.9","author":"jeromesteunenberg","date":"2026-03-27 17:07:43"}},"upgrade_notice":[],"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3529901,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3529901,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3525724,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3525724,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["0.0.10","0.0.11","0.0.12","0.0.13","0.0.14","0.0.15","0.0.16","0.0.17","0.0.18","0.0.19","0.0.20","0.0.21","0.0.22","0.0.23","0.0.24","0.0.3","0.0.4","0.0.5","0.0.6","0.0.7","0.0.8","0.0.9"],"block_files":[],"assets_screenshots":[],"screenshots":[],"jetpack_post_was_ever_published":false},"plugin_section":[],"plugin_tags":[74387,32397,216845,15326,1673],"plugin_category":[],"plugin_contributors":[258561],"plugin_business_model":[],"class_list":["post-220400","plugin","type-plugin","status-publish","hentry","plugin_tags-content-sync","plugin_tags-devops","plugin_tags-generateblocks","plugin_tags-git","plugin_tags-github","plugin_contributors-jeromesteunenberg","plugin_committers-jeromesteunenberg"],"banners":{"banner":"https:\/\/ps.w.org\/pushpull\/assets\/banner-772x250.png?rev=3525724","banner_2x":"https:\/\/ps.w.org\/pushpull\/assets\/banner-1544x500.png?rev=3525724","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/pushpull\/assets\/icon-128x128.png?rev=3529901","icon_2x":"https:\/\/ps.w.org\/pushpull\/assets\/icon-256x256.png?rev=3529901","generated":false},"screenshots":[],"raw_content":"<!--section=description-->\n<p>PushPull stores selected WordPress content in a Git repository using a canonical JSON representation instead of raw database dumps.<\/p>\n\n<p>This project is also documented through a DevOps-focused article series that explains how to efficiently manage a WordPress stack with Bedrock and PushPull, starting here: https:\/\/creativemoods.pt\/devops-with-wordpress\/<\/p>\n\n<h3>Beta notice<\/h3>\n\n<p>This is a beta plugin. It is still under active development, has limited functionality, and currently supports only a narrow subset of the intended PushPull feature set.<\/p>\n\n<p>The current release supports these managed domains:<\/p>\n\n<ol>\n<li>Primary domains:\n   generateblocks_global_styles\n   generateblocks_conditions\n   wordpress_block_patterns\n   wordpress_categories\n   wordpress_comments\n   wordpress_menus\n   wordpress_pages\n   wordpress_posts\n   wordpress_tags\n   wordpress_custom_css\n   generatepress_elements\n   wordpress_attachments (explicit opt-in only)\ngeneric discovered custom post types and taxonomies<\/li>\n<li>Config domains:\n   wordpress_core_configuration\n   wpml_configuration<\/li>\n<li>Overlay domains:\n   translation_management (WPML-backed)\n   media_organization (Real Media Library-backed)<\/li>\n<\/ol>\n\n<p>PushPull keeps a local Git-like repository inside WordPress database tables and supports the following workflow directly from WordPress admin:<\/p>\n\n<ol>\n<li>Test the remote GitHub or GitLab connection<\/li>\n<li>Commit live managed content into the local repository<\/li>\n<li>Initialize an empty remote repository<\/li>\n<li>Fetch remote commits into a local tracking ref<\/li>\n<li>Diff live, local, and remote states<\/li>\n<li>Pull remote changes through fetch + merge<\/li>\n<li>Merge remote changes into the local branch<\/li>\n<li>Resolve conflicts when needed<\/li>\n<li>Apply repository content back into WordPress<\/li>\n<li>Push local commits to GitHub or GitLab<\/li>\n<\/ol>\n\n<p>The plugin also includes:<\/p>\n\n<ol>\n<li>An audit log screen<\/li>\n<li>Local repository reset tooling<\/li>\n<li>Remote branch reset tooling that creates one commit removing all tracked files from the branch<\/li>\n<li>A dedicated Domains screen that separates WordPress core, installed plugin integrations, and discovered custom content<\/li>\n<li>Global and per-domain managed-content views in the admin UI<\/li>\n<li>A high-level PushPull status dropdown in the WordPress admin bar<\/li>\n<li>Menu structure export and apply with hierarchy and theme location assignment<\/li>\n<li>A scheduled lightweight remote-head check that highlights when <code>Fetch<\/code> likely has updates available<\/li>\n<li>Bulk <code>Commit + Push All<\/code> and <code>Pull + Apply All<\/code> workflows for whole-site bootstrap and deploy flows<\/li>\n<li>A <code>wp pushpull<\/code> WP-CLI command surface for status, configuration, domains, and sync operations<\/li>\n<\/ol>\n\n<h3>Current scope<\/h3>\n\n<p>This is an early, focused release. At the moment, PushPull is intentionally limited to:<\/p>\n\n<ol>\n<li>GitHub and GitLab as implemented remote providers<\/li>\n<li>Managed domains across three families:\n   generateblocks\/global-styles\/\n   generateblocks\/conditions\/\n   wordpress\/block-patterns\/\n   wordpress\/categories\/\n   wordpress\/comments\/\n   wordpress\/menus\/\n   wordpress\/pages\/\n   wordpress\/posts\/\n   wordpress\/tags\/\n   wordpress\/custom-css\/\n   wordpress\/attachments\/\n   wordpress\/configuration\/\n   wpml\/configuration\/\n   wordpress\/generatepress-elements\/\n   wordpress\/custom-post-types\/\/\n   wordpress\/custom-taxonomies\/\/\n   translations\/management\/\n   media\/organization\/<\/li>\n<li>Canonical JSON storage with one file per managed item for manifest-backed sets, plus directory-backed storage for attachments using <code>attachment.json<\/code> and the binary file<\/li>\n<li>Explicit opt-in attachment sync through a media-library checkbox<\/li>\n<li>Overlay domains that scope themselves to enabled compatible base domains rather than exporting every backend row blindly<\/li>\n<li>A cached remote-head availability signal for <code>Fetch<\/code>, driven by a configurable recurring check instead of a live provider probe on every page load<\/li>\n<\/ol>\n\n<p>It does not yet manage forms, users, arbitrary <code>wp_options<\/code>, or arbitrary plugin data.<\/p>\n\n<h3>How PushPull represents content<\/h3>\n\n<p>PushPull does not use WordPress post IDs as repository identity.<\/p>\n\n<p>For the currently supported managed sets it stores:<\/p>\n\n<ol>\n<li>One canonical JSON file per managed item<\/li>\n<li>One separate <code>manifest.json<\/code> file for manifest-backed sets that preserves logical ordering<\/li>\n<li>One directory per attachment for the attachments set, containing <code>attachment.json<\/code> and the binary file<\/li>\n<li>Stable logical keys instead of environment-specific database IDs<\/li>\n<li>Canonical logical-key references for cross-domain relationships such as reading settings, translation groups, media folders, GeneratePress condition targets, and menu object references<\/li>\n<li>Recursive placeholder normalization for current-site absolute URLs in post-type-backed content<\/li>\n<\/ol>\n\n<h3>Configuration<\/h3>\n\n<p>PushPull currently supports GitHub and GitLab repositories.<\/p>\n\n<p>For GitHub, grant:<\/p>\n\n<ol>\n<li>Repository metadata read access<\/li>\n<li>Repository contents read and write access<\/li>\n<\/ol>\n\n<p>For GitLab fine-grained personal access tokens, grant:<\/p>\n\n<ol>\n<li><code>Project: Read<\/code><\/li>\n<li><code>Branch: Read<\/code><\/li>\n<li><code>Commit: Read<\/code><\/li>\n<li><code>Commit: Create<\/code><\/li>\n<li><code>Repository: Read<\/code><\/li>\n<\/ol>\n\n<p>In PushPull &gt; Settings:<\/p>\n\n<ol>\n<li>Select <code>GitHub<\/code> or <code>GitLab<\/code> as the provider<\/li>\n<li>Enter the repository owner and repository name<\/li>\n<li>Enter the target branch<\/li>\n<li>Enter the API token<\/li>\n<li>Optionally set the remote fetch check interval in minutes<\/li>\n<li>Click <code>Test connection<\/code><\/li>\n<li>Save the settings<\/li>\n<\/ol>\n\n<h4>Domain selection<\/h4>\n\n<p>In PushPull &gt; Domains:<\/p>\n\n<ol>\n<li>Enable one or more managed domains<\/li>\n<li>Review WordPress core, installed plugin integrations, and discovered custom content separately<\/li>\n<li>Opt into generic discovered custom post types and taxonomies when you want them managed<\/li>\n<\/ol>\n\n<h4>Workflow<\/h4>\n\n<p>The normal workflow is:<\/p>\n\n<ol>\n<li><code>Commit<\/code> to snapshot the current live managed-set content into the local repository<\/li>\n<li><code>Fetch<\/code> to import the current remote branch into <code>refs\/remotes\/origin\/&lt;branch&gt;<\/code><\/li>\n<li>Inspect the live\/local and local\/remote diff views if needed<\/li>\n<li><code>Pull<\/code> for the common fetch + merge flow, or <code>Merge<\/code> manually after fetch when you want review first<\/li>\n<li><code>Apply repo to WordPress<\/code> when you want the local branch state written back into WordPress<\/li>\n<li><code>Push<\/code> when you want local commits published to GitHub or GitLab<\/li>\n<\/ol>\n\n<p>PushPull also performs a lightweight recurring remote-head check and visually highlights <code>Fetch<\/code> when the latest scheduled check suggests the remote branch has advanced since the last fetch.<\/p>\n\n<p>For whole-site bootstrap flows, PushPull also supports:<\/p>\n\n<ol>\n<li><code>Commit + Push All<\/code> to snapshot and publish all enabled domains<\/li>\n<li><code>Pull + Apply All<\/code> to import and apply all enabled domains on a bare target site<\/li>\n<\/ol>\n\n<p>If both local and remote changed, PushPull can persist conflicts, let you resolve them in the admin UI, and then finalize a merge commit.<\/p>\n\n<p>When pushing to GitLab, PushPull currently linearizes local merge results into a normal commit on the remote branch instead of preserving merge topology. The merged tree content is preserved; only the remote Git history shape is flattened.<\/p>\n\n<h4>WP-CLI<\/h4>\n\n<p>PushPull also exposes a <code>wp pushpull<\/code> command.<\/p>\n\n<p>Examples:<\/p>\n\n<ol>\n<li><code>wp pushpull status<\/code><\/li>\n<li><code>wp pushpull domains<\/code><\/li>\n<li><code>wp pushpull config list<\/code><\/li>\n<li><code>wp pushpull config set branch main<\/code><\/li>\n<li><code>wp pushpull config enable-domain wordpress_pages<\/code><\/li>\n<li><code>wp pushpull commit wordpress_pages<\/code><\/li>\n<li><code>wp pushpull push<\/code><\/li>\n<li><code>wp pushpull commit-push-all<\/code><\/li>\n<li><code>wp pushpull pull-apply-all<\/code><\/li>\n<\/ol>\n\n<h4>Empty repositories<\/h4>\n\n<p>If the configured GitHub or GitLab repository exists but has no commits yet, <code>Test connection<\/code> will report that the repository is reachable but empty.<\/p>\n\n<p>In that case, click <code>Initialize remote repository<\/code>. PushPull will:<\/p>\n\n<ol>\n<li>create the first commit on the configured branch<\/li>\n<li>fetch that initial commit into the local remote-tracking ref<\/li>\n<li>make the repository ready for normal commit, fetch, merge, apply, and push workflows<\/li>\n<\/ol>\n\n<p>You do not need to create the first commit manually on the provider before using PushPull.<\/p>\n\n<h3>TODO<\/h3>\n\n<ol>\n<li>Cache the admin-bar PushPull status summary so the high-level live\/local and local\/remote aggregation is not recomputed on every page view.<\/li>\n<li>Move chunked async provider resumability fully into the provider layer so <code>AsyncBranchOperationRunner<\/code> no longer needs provider-specific GitLab staging rehydration logic.<\/li>\n<li>Improve push progress and recap reporting to distinguish newly uploaded objects from objects reused from the remote history.<\/li>\n<li>Surface unresolved logical-reference mapping issues, such as GeneratePress condition IDs that could not be converted to logical placeholders, instead of silently leaving mixed raw IDs and canonical refs.<\/li>\n<\/ol>\n\n<h3>External services<\/h3>\n\n<p>PushPull connects to the GitHub or GitLab API for the repository you configure in the plugin settings.<\/p>\n\n<p>The plugin uses the provider REST API to:<\/p>\n\n<ol>\n<li>Read repository metadata and the default branch<\/li>\n<li>Read and update branch refs<\/li>\n<li>Read and create Git objects or provider-equivalent commit actions<\/li>\n<li>Test repository access before sync operations<\/li>\n<\/ol>\n\n<p>PushPull sends the following information to the configured provider over HTTPS:<\/p>\n\n<ol>\n<li>The repository owner, repository name, branch, and API base URL<\/li>\n<li>Your configured API token in the provider-specific authentication header<\/li>\n<li>Canonical JSON representations of the managed content you choose to commit and push<\/li>\n<li>Commit metadata such as commit messages and, if configured, author name and email<\/li>\n<\/ol>\n\n<p>In the current release, the managed content sent to the provider is limited to the enabled supported domains: GenerateBlocks Global Styles, GenerateBlocks Conditions, WordPress Block Patterns, WordPress Categories, WordPress Comments, WordPress Menus, WordPress Pages, WordPress Posts, WordPress Tags, WordPress Custom CSS, GeneratePress Elements, explicitly opted-in WordPress Attachments, WordPress core configuration, WPML configuration, generic discovered custom post types and taxonomies, WPML-backed translation management, and Real Media Library-backed media organization.<\/p>\n\n<p>PushPull does not send your whole WordPress database to the provider. It only sends the managed content represented by the enabled adapters.<\/p>\n\n<p>GitHub terms of service: https:\/\/docs.github.com\/en\/site-policy\/github-terms\/github-terms-of-service\nGitHub privacy statement: https:\/\/docs.github.com\/en\/site-policy\/privacy-policies\/github-general-privacy-statement\nGitLab terms: https:\/\/about.gitlab.com\/terms\/\nGitLab privacy statement: https:\/\/about.gitlab.com\/privacy\/<\/p>\n\n<!--section=installation-->\n<h4>Uploading in WordPress Dashboard<\/h4>\n\n<ol>\n<li>Download the plugin ZIP.<\/li>\n<li>In WordPress, go to Plugins &gt; Add New Plugin.<\/li>\n<li>Click Upload Plugin.<\/li>\n<li>Select the ZIP file.<\/li>\n<li>Install and activate the plugin.<\/li>\n<\/ol>\n\n<h4>Installing from source<\/h4>\n\n<ol>\n<li>Clone this repository into <code>wp-content\/plugins\/pushpull<\/code>.<\/li>\n<li>Run <code>composer install<\/code>.<\/li>\n<li>Activate PushPull in WordPress.<\/li>\n<\/ol>\n\n<h4>Install via Composer \/ Packagist<\/h4>\n\n<p>In a Composer-managed WordPress project such as Bedrock:<\/p>\n\n<ol>\n<li>Require the plugin:\n   composer require creativemoods\/pushpull<\/li>\n<li>Make sure the root project allows <code>composer\/installers<\/code> and installs <code>type: wordpress-plugin<\/code> packages into your plugins directory<\/li>\n<li>Activate PushPull in WordPress.<\/li>\n<\/ol>\n\n<!--section=changelog-->\n<p>See the full release history in <code>CHANGELOG.md<\/code>.<\/p>","raw_excerpt":"Git-based content sync for WordPress. Project homepage: https:\/\/creativemoods.pt\/pushpull\/","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/ga.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/220400","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ga.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/ga.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/ga.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=220400"}],"author":[{"embeddable":true,"href":"https:\/\/ga.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/jeromesteunenberg"}],"wp:attachment":[{"href":"https:\/\/ga.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=220400"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/ga.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=220400"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/ga.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=220400"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/ga.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=220400"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/ga.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=220400"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/ga.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=220400"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}