February 17, 2009

Depot blogging with P4 Wordpress

What's New

Update: this plugin is no longer available/supported.

As a developer you most likely have a well-defined SCM workflow and toolset that you rely on for collaborative development. You become accustomed to using specific tools to track changes made by others, compare revision differences, view file history, etc.. Now you decide to start blogging and you find yourself using an application like WordPress with its own versioning system, file hierarchy, and diffing system. This is the exact problem I encountered when I first decided to contribute to the Perforce blog. As my posts evolved and grew I quickly saw the limits of the basic revisioning capabilities included in WordPress and decided to create a plugin that puts all WordPress assets into a Perforce depot. This then allowed me to use the same Perforce applications for my blog posts that I use every day for P4WSAD development.

The P4 Wordpress plugin is an attempt at making the act of blogging fit into your typical SCM workflow. It allows you to leverage existing Perforce tools for content added to Wordpress whether it is a blog post, a static page, or uploaded media such as screenshots or videos. P4 Wordpress uses the Wordpress action hooks to execute corresponding Perforce server commands when content is added, edited, or deleted. It then becomes completely transparent to the blogger that the content they are creating is simultaneously being saved to the depot each time they save a draft.

The P4 WordPress plugin is a small PHP file that works with WordPress 2.7+. It hooks into five WordPress events to perform p4 add, edit, and delete commands when a post, page, or attachment is modified from the WordPress Dashboard. The plugin uses other WordPress APIs to read post content from the database and save it to a file that is then submitted to the depot. Posts and pages are saved as text files named by their WordPress ID and contain both the title and content of the post separated by a newline.

The plugin creates depot folders for the three types of content inside WordPress: posts, pages, and attachments. Each user that adds content to your Wordpress installation will have a post and page folder for all the content that they create. You can then use P4V and the Submitted or History tab to view their posting history (pictured below).

P4V view of a user's blogging history
P4V view of a user's blogging history

Another benefit of putting your WordPress content in the depot is that you can now leverage other Perforce tools such as the P4V Time Lapse view to quickly see the entire history of a selected blog post or static page (pictured below).

P4V Time Lapse view for a blog post
P4V Time Lapse view for a blog post

By using this plugin you can also now utilize your existing depot backup plan for blog content instead of worrying about backing up WordPress content independently of your depot. You may still want to backup certain WordPress databases for things such as users and comments.

You can re-configure the plugin by editing the get_client_path method to setup a different depot hierarchy for users and types of content by editing. Also you can further extend the changelist description used if you wanted to include more metadata from the post by editing the string added after the "-d" flag when a submit command is executed. Installation:

  • Download the P4 WordPress plugin as p4wp.php
  • Configure the following constants at the top of the file:
    • $P4USER = <The Perforce user to use when adding, editing and deleting files>
    • $P4CLIENT = <The Perforce client to use when adding, editing, and deleting files. It is very important that this client use the SubmitOptions: revertunchanged option. This will prevent duplicate revisions from being in the depot if a post is saved without any changes>
    • $P4SERVER = <The Perforce server to use when adding, editing, and deleting files>
    • $CLIENTROOT = <The local path under your Perforce client workspace to put all blog files under>
    • $P4CMD = <Path to the p4 executable>
    • $LOG_LOCATION = <File path to log errors to>
  • Install the plugin into your Wordpress Installation


  • Attachments are not stored under a folder for the user who uploaded the file. This is due to a limitation in WordPress where the delete attachment event fires after it has already been deleted so by the time the event hook runs all the database information has already been deleted. That is why the delete_attachment_from_depot use an asterisk when deleting the attachment since WordPress only provides the ID, not the path. It looks like this will be fixed in WordPress 2.8 so starting in that release the attachment folder could have the same hierarchy as the post and page folders.
  • WordPress does not seem to have a special add event for the first time a post/page is added so instead I used the transition post status event as that seems to fire when a post/page is first added.

P4 Wordpress plugin source:

<?php/** * @package com.perforce.p4wp * @author Kevin Sawicki * @version 1.0 *//*Plugin Name: P4 WordPressPlugin URI: http://www.perforce.com/Description: Perforce WordPress pluginAuthor: Kevin SawickiVersion: 1.0Author URI: http://www.perforce.com/*/$P4USER="ksawicki";$P4CLIENT="ksawicki_local";$P4SERVER="localhost:1666";$CLIENTROOT="/Users/ksawicki/Perforce/blog/";$P4CMD="p4";$P4BASE=$P4CMD." -u ".$P4USER." -p ".$P4SERVER." -c ".$P4CLIENT;$LOG_LOCATION="/var/tmp/p4wp_error.txt";functionget_client_path($author,$type,$id){global$CLIENTROOT;return$CLIENTROOT.$type."/".$author."/".$id.".txt";}functionadd_file_to_depot($author,$clientPath,$unicode){global$P4BASE;$desc=array(0=>array("pipe","r"),1=>array("pipe","w"),2=>array("pipe","w"),);//Open for add$command=$P4BASE." add ";if($unicode){$command.="-t unicode ";}$command.=$clientPath;$process=proc_open($command,$desc,$pipes, null,$_ENV);$rc=proc_close($process);//Submit$command=$P4BASE." submit -d \"Added by ".$author."\" \"".$clientPath."\"";$process=proc_open($command,$desc,$pipes, null,$_ENV);$rc=proc_close($process);}functiondelete_file_from_depot($author,$clientPath){global$P4BASE;$desc=array(0=>array("pipe","r"),1=>array("pipe","w"),2=>array("pipe","w"),);//Open for add$command=$P4BASE." delete ";$command.=$clientPath;$process=proc_open($command,$desc,$pipes, null,$_ENV);$rc=proc_close($process);//Submit$command=$P4BASE." submit -d \"Deleted by ".$author."\" \"".$clientPath."\"";$process=proc_open($command,$desc,$pipes, null,$_ENV);$rc=proc_close($process);}functionedit_file_in_depot($author,$clientPath,$content){global$CHANGESPEC,$P4BASE;$desc=array(0=>array("pipe","r"),1=>array("pipe","w"),2=>array("pipe","w"),);//Open for edit$command=$P4BASE." edit ";$command.=$clientPath;$process=proc_open($command,$desc,$pipes, null,$_ENV);$rc=proc_close($process);//Copy file$fp=fopen($clientPath,'w');fwrite($fp,$content);fclose($fp);//Submit$command=$P4BASE." submit -d \"Edited by ".$author."\" \"".$clientPath."\"";$process=proc_open($command,$desc,$pipes, null,$_ENV);$rc=proc_close($process);}functionedit_post_in_depot($post_ID,$post){global$LOG_LOCATION;if(!isset($post)){return;}$id=$post_ID;$title=$post->post_title;$content=$post->post_content;$author=get_usermeta($post->post_author,'user_login');$type=$post->post_type;

	try {$local=get_client_path($author,$type,$id);edit_file_in_depot($author,$local,"Title:\n".$title."\n\nContent:\n".$content);}catch (Exception $e){error_log($e->getMessage()."\n",3,$LOG_LOCATION);}}functionadd_post_to_depot($new_status,$old_status,$post){global$CLIENTROOT,$LOG_LOCATION;if(!isset($post)){return;}if($old_status=='new'&&$new_status=='draft'){$id=$post->ID;$title=$post->post_title;$author=get_usermeta($post->post_author,'user_login');$type=$post->post_type;

		try {$local=get_client_path($author,$type,$id);if(!file_exists($CLIENTROOT.$type)){mkdir($CLIENTROOT.$type);}if(!file_exists($CLIENTROOT.$type."/".$author)){mkdir($CLIENTROOT.$type."/".$author);}//Copy file$fp=fopen($local,'w');fwrite($fp,"Title:\n".$title);fclose($fp);add_file_to_depot($author,$local, true);}catch (Exception $e){error_log($e->getMessage()."\n",3,$LOG_LOCATION);}}}functionremove_from_depot($post_ID){global$LOG_LOCATION;$id=$post_ID;$post=get_post($id);$author=get_usermeta($post->post_author,'user_login');$type=$post->post_type;

	try {$local=get_client_path($author,$type,$id);delete_file_from_depot($author,$local);}catch (Exception $e){error_log($e->getMessage()."\n",3,$LOG_LOCATION);}}functionadd_attachment_to_depot($attachment_ID){global$CLIENTROOT,$LOG_LOCATION;$id=$attachment_ID;$post=get_post($id);$author=get_usermeta($post->post_author,'user_login');$type="attachment";

	try {if(!file_exists($CLIENTROOT.$type)){mkdir($CLIENTROOT.$type);}//Copy file$attachmentPath=get_attached_file($attachment_ID);$local=$CLIENTROOT.$type."/".$attachment_ID."_".basename($attachmentPath);copy($attachmentPath,$local);add_file_to_depot($author,$local, false);}catch (Exception $e){error_log($e->getMessage()."\n",3,$LOG_LOCATION);}}functiondelete_attachment_from_depot($attachment_ID){global$CLIENTROOT,$LOG_LOCATION;$id=$attachment_ID;$type="attachment";

	try {$attachmentPath=get_attached_file($attachment_ID);$local=$CLIENTROOT.$type."/".$attachment_ID."_*";delete_file_from_depot($type,$local);}catch (Exception $e){error_log($e->getMessage()."\n",3,$LOG_LOCATION);}}add_action('edit_post','edit_post_in_depot',10,2);add_action('delete_post','remove_from_depot');add_action('transition_post_status','add_post_to_depot',10,3);add_action('add_attachment','add_attachment_to_depot');add_action('delete_attachment','delete_attachment_from_depot');?>