Redirect BuddyPress activity reply links to forum’s “Leave a Reply”

Activity stream replies in BuddyPress are pretty cool, but they have the potential to be confusing. On the CUNY Academic Commons, we have disabled activity replies for activity entries related to blogs and forums, because allowing replies in these cases has the potential to confuse users and fracture conversation.

There are a number of ways that this could (and should, and will!) be improved in future versions of BuddyPress. But, for now, here’s a trick. The following code will change the behavior of the Reply buttons for forum-related activity entries (new forum topics, and forum topic replies), so that instead of sliding down the inline activity comment box, it goes to the Reply form on the forum topic itself.

Side note: This seems like it’d be an easy thing to do, but it turns out to be somewhat complex. As I explain in the inline documentation, the issue of pagination means that there’s no predictable way to easily concatenate a URL for a topic’s reply box (this is one of the things I want to fix in BP core) – you have to fetch the number of total replies and figure out the last page from there. Also, in the case of topic replies, you have to do an additional query to get the id of the topic that the post belongs to, because that info is not stored in the activity table. The function cac_insert_comment_reply_links() below tries to consolidate these lookups to add as few queries as possible to the pageload.

Second side note: This code is not particularly beautiful. It makes direct queries to the bbPress database tables. So sue me.

OK, so the code itself. First, put this chunk into your bp-custom.php file.

/**
 * Gets accurate reply URLs for the activity stream
 *
 * Getting accurate Reply links for forum topics is tricky because of pagination - you need to know
 * how many total posts are in the topic so that you can figure out what the last page should be.
 * Moreover, the forum reply activity items don't have the topic_id stored with them. This function
 * attempts to minimize DB queries by looking up all topic_ids at once, then looking up all post
 * counts at once - adding 2 queries for the activity loop is better than 20.
 *
 * Todo: Get a real redirecter into BuddyPress itself
 */
function cac_insert_comment_reply_links( $has_comments ) {
	global $activities_template, $wpdb, $bbdb;

	do_action( 'bbpress_init' );

	$topics_data = array();
	$posts_data = array();
	foreach( $activities_template->activities as $key => $activity ) {
		if ( $activity->type == 'new_forum_topic' ) {
			$topic_id = $activity->secondary_item_id;
			$topics_data[$topic_id]['url'] = $activity->primary_link;
			$topics_data[$topic_id]['activity_key'] = $key;	
		}

		if ( $activity->type == 'new_forum_post' ) {
			$post_id = $activity->secondary_item_id;
			$posts_data[$post_id]['url'] = array_pop( array_reverse( explode( '#', $activity->primary_link ) ) );
			$posts_data[$post_id]['activity_key'] = $key; 
		}
	}

	// In cases where we only have the post id, we must do an extra query to get topic ids
	if ( !empty( $posts_data ) ) {
		$post_ids 	= array_keys( $posts_data );
		$post_ids_sql 	= implode( ',', $post_ids );
		$sql 		= $wpdb->prepare( "SELECT topic_id, post_id FROM {$bbdb->posts} WHERE post_id IN ({$post_ids_sql})" );
		$post_topic_ids = $wpdb->get_results( $sql );

		// Now that we have the topic IDs, we can add that info to $topics_data for the main query
		foreach( $post_topic_ids as $post_topic ) {
			$topics_data[$post_topic->topic_id] = $posts_data[$post_topic->post_id];
		}
	}

	// Now for the main event
	// First, make a topic list and get all the associated posts
	$topic_ids 	= implode( ',', array_keys( $topics_data ) );
	$sql		= $wpdb->prepare( "SELECT topic_id, post_id FROM {$bbdb->posts} WHERE topic_id IN ({$topic_ids})" );
	$posts		= $wpdb->get_results( $sql );

	// Now we get counts. BTW it sucks to do it this way
	$counter	= array();
	foreach( $posts as $post ) {
		if ( empty( $counter[$post->topic_id] ) )
			$counter[$post->topic_id] = 1;
		else
			$counter[$post->topic_id]++;
	}

	// Finally, concatenate the reply url and put it in the activities_template
	foreach( $topics_data as $topic_id => $data ) {
		$total_pages = ceil( $counter[$topic_id] / 15 );	
		$reply_url = cac_forum_reply_url( $data['url'], $total_pages, 15 );
		$key = $data['activity_key'];
		$activities_template->activities[$key]->reply_url = $reply_url;
	}

	return $has_comments;
}
add_action( 'bp_has_activities', 'cac_insert_comment_reply_links' );

/**
 * Filters the url of the activity reply link to use reply_url, if present
 */
function cac_filter_activity_reply_link( $link ) {
	global $activities_template;

	if ( !empty( $activities_template->activity->reply_url ) )
		return $activities_template->activity->reply_url;
	else
		return $link;
}
add_action( 'bp_get_activity_comment_link', 'cac_filter_activity_reply_link' );

/**
 * Echoes the proper CSS class for the activity reply link. This is necessary to ensure that 
 * the JS slider does not appear when we have a custom reply_url.
 */
function cac_activity_reply_link_class() {
	global $activities_template;

	if ( !empty( $activities_template->activity->reply_url ) )
		echo 'class="acomment-reply-nojs"';
	else
		echo 'class="acomment-reply"';
}

/**
 * A replacement for bp_activity_can_comment(). Todo: deprecate into a filter when BP 1.3 comes out
 */
function cac_activity_can_comment() {
	global $activities_template, $bp;

	if ( false === $activities_template->disable_blogforum_replies || (int)$activities_template->disable_blogforum_replies ) {
		// If we've got a manually created reply_url (see cac_insert_comment_reply_links(), return true
		if ( !empty( $activities_template->activity->reply_url ) )
			return true;

		if ( 'new_blog_post' == bp_get_activity_action_name() || 'new_blog_comment' == bp_get_activity_action_name() || 'new_forum_topic' == bp_get_activity_action_name() || 'new_forum_post' == bp_get_activity_action_name() )
			return false;
	}
	
	return true;
}

You’ll note that there are a few places in that code where the number 15 is mentioned explicitly. I’m assuming that you’re using 15 posts-per-page for your single topic pagination. You can change this number accordingly if you want.

Next, you’ll have to make a few changes in your theme’s activity/entry.php to account for the changes. There are two relevant changes. First, you’ll be removing the activity reply button’s CSS class (hardcoded by default) and replacing it with the dynamically generated version in cac_activity_reply_link_class(). Second, you’ll be swapping out the checks for bp_activity_can_comment() with cac_activity_can_comment(), so that you can still block blog-activity comments. The code below is lines 27-29 of my activity/entry.php – you should be able to figure out which lines to replace with the following, as I haven’t changed much.

<?php if ( is_user_logged_in() &amp;&amp; cac_activity_can_comment() ) : ?>
	<a href="<?php bp_activity_comment_link() ?>" <?php cac_activity_reply_link_class() ?> id="acomment-comment-<?php bp_activity_id() ?>"><?php _e( 'Reply', 'buddypress' ) ?> (<span><?php bp_activity_comment_count() ?></span>)</a>
<?php endif; ?>

Finally, because you’ve changed the CSS selector on some of the reply buttons, you’ll want to add some styles to your stylesheet. These are borrowed right from bp-default.

.activity-list div.activity-meta a.acomment-reply-nojs {
	background: #fff9db;
	border-bottom: 1px solid #ffe8c4;
	border-right: 1px solid #ffe8c4;
	color: #ffa200;
}

div.activity-meta a.acomment-reply-nojs:hover {
	background: #f7740a;
	color: #fff;
    border-color: #f7740a;
}

Good luck!

Commons 1.1.10

I have just released version 1.1.10 of the CUNY Academic Commons. About a month has passed since 1.1.9, and in that time, a lot of small fixes and enhancements have taken place. Of note:

  • WordPress and BuddyPress have been updated to the most recent versions (3.1 and 1.2.8, respectively)
  • New WP theme: Bibliotype
  • New WP plugins: ZotPress (for integration with your Zotero library); List Pages Shortcode (for displaying page hierarchy on your blog)
  • Fixes to the way that group blog membership is synched with group membership
  • Sticky forum posts fixed so that stickies don’t get pushed down to the second page of a group forum

You can read about the release in a more in-depth way by checking out the 1.1.10 milestone.

Hardening BuddyPress Group Documents

The BuddyPress Group Documents plugin allows groups a handy way for users to share documents with fellow members of a BP group. It’s crucial to the work that is done on the CUNY Academic Commons. But, by default, the plugin stores documents in a subdirectory of your WP uploads folder (usually /wp-content/blogs.dir/ on multisite). That means that documents are available directly, to anyone who has the URL, regardless of the public/private/hidden status of groups. This isn’t a problem from within BuddyPress, since URLs for the documents only appear inside of the protected group interface. But if the URL is shared, then the document becomes publicly available. Worse, if someone posts the URL of a document in a public place, search engine bots will find it, and the contents of the document could end up in Google.

I wrote a few helper functions to change this behavior. The strategy is this: Move the files so that they are not accessible via URL, ie in a directory above the web root. (In my case, it’s a directory called bp-group-documents, just above my web root.) Then, catch requests of a certain type (I’ve chosen to go with a URL parameter get_group_doc=), and check them to see whether the current user has the adequate permission to access the document in question. Finally, make sure that all of the URLs and paths that BPGD uses to upload and display documents are filtered to the updated versions. I’ve provided my code below – use and modify at your pleasure. You should be able to place it in your plugins/bp-custom.php file, and then move your existing docs from their current location (probably something like wp-content/blogs.dir/1/files/group-documents) to the new directory.

I also added a line to my .htaccess file to ensure that requests to the old URLs are redirected to the new, hardened URL. That line is this:


RewriteRule ^wp\-content/blogs\.dir/1/files/group\-documents/(.*) /?get_group_doc=$1 [R,L]
 

Obviously, you may have to modify it for different file paths.

EDITED Feb 8, 2011 to include the code for creating directories when none exist


define( 'BP_GROUP_DOCUMENTS_SECURE_PATH', substr( ABSPATH, 0, strrpos( rtrim( ABSPATH, '/' ), '/' ) ) . '/bp-group-documents/' );

function cac_filter_doc_url( $doc_url, $group_id, $file ) {
	$url = bp_get_root_domain() . '?get_group_doc=' . $group_id . '/' . $file;
	return $url;
}
add_filter( 'bp_group_documents_file_url', 'cac_filter_doc_url', 10, 3 );

function cac_filter_doc_path( $doc_url, $group_id, $file ) {
	$document_dir = BP_GROUP_DOCUMENTS_SECURE_PATH . $group_id . '/';
	
	if ( !is_dir( $document_dir ) )
		mkdir( $document_dir, 0775, true );

	$path = BP_GROUP_DOCUMENTS_SECURE_PATH . $group_id . '/' . $file;
	return $path;
}
add_filter( 'bp_group_documents_file_path', 'cac_filter_doc_path', 10, 3 );

function cac_catch_group_doc_request() {
	$error = false;

	if ( empty( $_GET['get_group_doc'] ) )
		return;
	
	$doc_id = $_GET['get_group_doc'];
	
	// Check to see whether the current user has access to the doc in question
	$file_deets 	= explode( '/', $doc_id );
	$group_id 	= $file_deets[0];	
	$group		= new BP_Groups_Group( $group_id );
	
	if ( empty( $group->id ) ) {
		$error = array(
			'message' 	=> 'That group does not exist.',
			'redirect'	=> bp_get_root_domain()
		);
	} else {
		if ( $group->status != 'public' ) {
			// If the group is not public, then the user must be logged in and
			// a member of the group to download the document
			if ( !is_user_logged_in() || !groups_is_user_member( bp_loggedin_user_id(), $group_id ) ) {
				$error = array(
					'message' 	=> sprintf( 'You must be a logged-in member of the group %s to access this document. If you are a member of the group, please log into the site and try again.', $group->name ),
					'redirect'	=> bp_get_group_permalink( $group )
				);
			}
		}
		
		// If we have gotten this far without an error, then the download can go through
		if ( !$error ) {
			
			$doc_path = BP_GROUP_DOCUMENTS_SECURE_PATH . $doc_id;
			
			if ( file_exists( $doc_path ) ) {
				$mime_type = mime_content_type( $doc_path );
				$doc_size = filesize( $doc_path );
				
				header("Cache-Control: public, must-revalidate, post-check=0, pre-check=0");
				header("Pragma: hack");
					
				header("Content-Type: $mime_type; name='" . $file_deets[1] . "'");
				header("Content-Length: " . $doc_size );
				
				header('Content-Disposition: attachment; filename="' . $file_deets[1] . '"');
				header("Content-Transfer-Encoding: binary");              
				ob_clean();
				flush();  		
				readfile( $doc_path );
				die();
		       
			} else {
				// File does not exist
				$error = array(
					'message' 	=> 'The file could not be found.',
					'redirect'	=> bp_get_group_permalink( $group ) . '/documents'
				);
			}
		}
	}
		
	// If we have gotten this far, there was an error. Add a message and redirect
	bp_core_add_message( $error['message'], 'error' );
	bp_core_redirect( $error['redirect'] );
}
add_filter( 'wp', 'cac_catch_group_doc_request', 1 );

// http://www.php.net/manual/en/function.mime-content-type.php#87856
if(!function_exists('mime_content_type')) {

    function mime_content_type($filename) {

        $mime_types = array(

            'txt' => 'text/plain',
            'htm' => 'text/html',
            'html' => 'text/html',
            'php' => 'text/html',
            'css' => 'text/css',
            'js' => 'application/javascript',
            'json' => 'application/json',
            'xml' => 'application/xml',
            'swf' => 'application/x-shockwave-flash',
            'flv' => 'video/x-flv',

            // images
            'png' => 'image/png',
            'jpe' => 'image/jpeg',
            'jpeg' => 'image/jpeg',
            'jpg' => 'image/jpeg',
            'gif' => 'image/gif',
            'bmp' => 'image/bmp',
            'ico' => 'image/vnd.microsoft.icon',
            'tiff' => 'image/tiff',
            'tif' => 'image/tiff',
            'svg' => 'image/svg+xml',
            'svgz' => 'image/svg+xml',

            // archives
            'zip' => 'application/zip',
            'rar' => 'application/x-rar-compressed',
            'exe' => 'application/x-msdownload',
            'msi' => 'application/x-msdownload',
            'cab' => 'application/vnd.ms-cab-compressed',

            // audio/video
            'mp3' => 'audio/mpeg',
            'qt' => 'video/quicktime',
            'mov' => 'video/quicktime',

            // adobe
            'pdf' => 'application/pdf',
            'psd' => 'image/vnd.adobe.photoshop',
            'ai' => 'application/postscript',
            'eps' => 'application/postscript',
            'ps' => 'application/postscript',

            // ms office
            'doc' => 'application/msword',
            'rtf' => 'application/rtf',
            'xls' => 'application/vnd.ms-excel',
            'ppt' => 'application/vnd.ms-powerpoint',

            // open office
            'odt' => 'application/vnd.oasis.opendocument.text',
            'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
        );

        $ext = strtolower(array_pop(explode('.',$filename)));
        if (array_key_exists($ext, $mime_types)) {
            return $mime_types[$ext];
        }
        elseif (function_exists('finfo_open')) {
            $finfo = finfo_open(FILEINFO_MIME);
            $mimetype = finfo_file($finfo, $filename);
            finfo_close($finfo);
            return $mimetype;
        }
        else {
            return 'application/octet-stream';
        }
    }
}


 

Commons 1.1.8

I’ve just released version 1.1.8 of the CUNY Academic Commons. This upgrade was a bit bumpy – I actually pushed the update yesterday afternoon, but because of a few technical problems, I was not able to debug and tag the upgrade as stable. Thanks to some help from our delightful sysadmin @apitanga, we sorted things out and I was able to wrap up the release.

Commons 1.1.8 features a relatively large number of fixes and improvements:

  • Many plugins and themes, including BuddyPress, have been upgraded to the most recent versions
  • A large number of new themes have been added to the site
  • Embedding of Prezi and Slideshare presentations has been enabled
  • A bug blocking group members from being auto-added to the group’s blog has been fixed
  • A few other small bugs fixed

For complete details on this release, see the 1.1.8 milestone.

Wildcard email whitelists in WordPress and BuddyPress

WordPress (and before that WPMU) has long had a feature that allows admins to set a whitelist of email domains for registration (Limited Email Registration). On the Commons, we need to account for a lot of different domains, some of which are actually dynamic – but they are all of the form *.cuny.edu. WP doesn’t support this kind of wildcards, but we’ve got it working through a series of customizations.

These first two functions form the heart of the process. The first one hooks to the end of the BP registration process, looks for email domain errors, and then sends the request to the second function, which does some regex to check against the wildcard domains you’ve specified. This is BP-specific, but I think you could make it work with WPMS just by changing the hook name.


function cac_signup_email_filter( $result ) {
	global $limited_email_domains;

	if ( !is_array( $limited_email_domains ) )
		$limited_email_domains = get_site_option( 'limited_email_domains' );
	
	$valid_email_domain_check = cac_wildcard_email_domain_check( $result['user_email'] );	
	
	if( $valid_email_domain_check ) {
		if ( isset( $result['errors']->errors['user_email'] ) )
			unset( $result['errors']->errors['user_email'] );
	}
	
	return $result;
}
add_filter( 'bp_core_validate_user_signup', 'cac_signup_email_filter', 8 );

function cac_wildcard_email_domain_check( $user_email ) {
	global $limited_email_domains;
	
	if ( !is_array( $limited_email_domains ) )
		$limited_email_domains = get_site_option( 'limited_email_domains' );

	if ( is_array( $limited_email_domains ) && empty( $limited_email_domains ) == false ) { 
		$valid_email_domain_check = false;
		$emaildomain = substr( $user_email, 1 + strpos( $user_email, '@' ) );
		foreach ($limited_email_domains as $limited_email_domain) {
			$limited_email_domain = str_replace( '.', '\.', $limited_email_domain);        // Escape your .s
			$limited_email_domain = str_replace('*', '[-_\.a-zA-Z0-9]+', $limited_email_domain);     // replace * with REGEX for 1+ occurrence of anything
			$limited_email_domain = "/^" . $limited_email_domain . "/";   // bracket the email with the necessary pattern markings
			$valid_email_domain_check = ( $valid_email_domain_check or preg_match( $limited_email_domain, $emaildomain ) );
		}
	}	

	return $valid_email_domain_check;
}

Before WP 3.0, this was enough to make it work. The latest WP does increased sanitization on the input of the limited_email_domains field, however, which makes it reject lines like *.cuny.edu. The following functions add an additional field to the ms-options.php panel that saves the limited domains without doing WP’s core checks. (Beware: bypassing WP’s checks like this means that there are no safeguards in place for well-formedness. Be careful about what you type in the field, or strange things may happen.)


function cac_save_limited_email_domains() {
	if ( $_POST['cac_limited_email_domains'] != '' ) {
		$limited_email_domains = str_replace( ' ', "\n", $_POST['cac_limited_email_domains'] );
		$limited_email_domains = split( "\n", stripslashes( $limited_email_domains ) );
	
		$limited_email = array();
		foreach ( (array) $limited_email_domains as $domain ) {
				$domain = trim( $domain );
			//if ( ! preg_match( '/(--|\.\.)/', $domain ) && preg_match( '|^([a-zA-Z0-9-\.])+$|', $domain ) )
				$limited_email[] = trim( $domain );
		}
		update_site_option( 'limited_email_domains', $limited_email );
	} else {
		update_site_option( 'limited_email_domains', '' );
	}
}
add_action( 'update_wpmu_options', 'cac_save_limited_email_domains' );

function cac_limited_email_domains_markup() {
	?>
	
	<h3><?php _e( 'Limited Email Domains That Actually Work' ); ?></h3>
	
	<table class="form-table">
	<tr valign="top">
		<th scope="row"><label for="cac_limited_email_domains"><?php _e( 'Limited Email Registrations' ) ?></label></th>
		<td>
			<?php $limited_email_domains = get_site_option( 'limited_email_domains' );
			$limited_email_domains = str_replace( ' ', "\n", $limited_email_domains ); ?>
			<textarea name="cac_limited_email_domains" id="limited_email_domains" cols="45" rows="5">< ?php echo wp_htmledit_pre( $limited_email_domains == '' ? '' : implode( "\n", (array) $limited_email_domains ) ); ?>
			<br />
			<?php _e( 'If you want to limit site registrations to certain domains. One domain per line.' ) ?>
		</td>
	</tr>
	</table>
	
	<?php
}
add_action( 'wpmu_options', 'cac_limited_email_domains_markup' );

Commons v1.0.4

Today I released version 1.0.4 of the CUNY Academic Commons. This minor release consists of a single fix: the addition of a plugin to displayed featured content on our homepage. The plugin, CAC Featured Content, was developed especially for the Academic Commons by Michael McManus of Cast Iron Coding, and is available for download in the WP plugin repo. Look for more details on that plugin in an upcoming blog post.

For more details on the 1.0.4 upgrade, please see the changelog.

Commons v1.0.2

I just finished upgrading the Commons to version 1.0.2, our second bugfix release this week!

This release contains the following small but important fixes. Of note: Bringing back the wiki search bar that had gone missing in v1.0; fixing the wiki Tags extension that wasn’t displaying the tags box on new wiki pages; fixing a bug that kept the “My Blogs” menu from being updated after you changed the name of your blog; ironing out wrinkles with the way that CUNY email addresses are filtered during registration.

See the 1.0.2 milestone for more details about what’s new.

Commons v1.0.1

Yesterday marked the release of Commons v1.0.1, containing several important bug fixes. The most obvious change is the new pictures and text on the front page slideshow. There are also changes to the language on certain parts of the Dashboard, some fixes to group blog functionality, group document email notification formatting fixes, and some styling tweaks.

See the 1.0.1 milestone on our bugtracker for more information.

The technical side of Commons v1.0

As Matt @admin announced earlier today, we’ve just tagged the CUNY Academic Commons “version 1.0”.

The 1.0 milestone represents a couple of big changes in our development processes.

  1. Versioning

    The most obvious change is the use of version numbers. It might seem at first like a relatively minor and superficial alteration in the way we operate. In truth, it’s a big change.

    Prior to the switch, our development cycles were haphazard at best. Bugs would be fixed and features implemented whenever someone on the development team happened to get around to it. That system (or lack thereof) works pretty well for a small team. But the team behind the Commons has been growing steadily over the past year, both in sheer number and in breadth (of geographical distribution, of areas of technical expertise, of responsibility). Greater distribution requires more thoughtful coordination and communication, and releasing discrete versions is a tool that we hope will help us achieve these goals.

    There are many benefits to discrete versioning. Here are a few prominent ones, off the top of my head:

    • Improved ticket management · Our ticketing system works best when tasks can be clearly assigned to individual developers, and organized according to priority. Version numbers provide a framework for this kind of management, as important tickets get assigned to earlier versions, and developer workload balance for a given release can be easily assessed.
    • Improved roadmapping · Roadmaps – software development language for “schedule of upcoming features” – are easier to understand and to implement when they’re organized by release numbers instead of thrown into a single pile.
    • Improved testing · When features and bugfixes are rolled out one-at-a-time, it can mean near-nonstop testing for the development and community team. Having controlled releases means that testing for a whole set of changes can be managed at the same time, in the days leading up to the release itself. That’s a better use of everyone’s time.
  2. Better distinction between development, staging, and production

    In those halcyon days early in the life of the Commons, nearly all development was done in the live production environment. When fixing a bug or launching a feature, I’d edit the site while it was up and running. Danger, Will Robinson! So many things can go wrong when you’re editing a live site, it’s hardly worth enumerating.

    The dev team eventually did set up a development environment. But it was running on a different server from the production site, and so you could never be guaranteed that a fix on the dev site would translate to the live site. Moreover, all those little fixes that would get sneaked directly onto the production site meant the dev site and the production site got progressively more out of synch as time went on, until the dev site was sufficiently different from the live site to be next to useless as a development site.

    Our new setup is much more sophisticated and, we hope, more conducive to quality development. We have a staging environment running in exactly the same operating environment as the production environment. Development environments are all local, which is to say that each member of the development team has a functional copy of the site on his personal computer. To whatever extent possible, development takes place in those local environments, is tested in our staging environment to look for environment-specific bugs, and only then pushed to the production environmnet. The (lofty) goal is that we’ll never have to make tweaks directly to the production site.

  3. Version control

    By “version control” I mean something a bit different from (though related to) “versioning” as mentioned above. Version control is a way to keep track of every change made to the files. It used to be that, when I tweaked a template or plugin file, unless I made a backup, the previous version would be overwritten and lost. Now that we’re using Git, we can always revert changes if need be (or browse the source out of mere historical curiosity!).

    The need for version control is all the more pressing as our development team grows. It’s important to keep track of who is changing what. And when you have local, independent development environments, it’s imperative that you have a way of making sure that the changes that I make on my version are not overwritten by the changes made by another member of the team when we merge them together in the staging environment. Git is designed for this kind of distributed development, as it keeps careful track of who edited what (‘git blame’, appropriately enough) and allows graceful merging of disparate edits.

    Using Git with WordPress and MediaWiki posed something of a technical challenge. For one thing, we wanted to track the entire codebase, but not files that users had uploaded to the system. Furthermore, MW and WP store crucial configuration information in the database, which is not tracked by Git. Finally, in order for local development environments to work properly, some environment-specific variables (database passwords, for instance) needed to be abstracted out of tracked files like wp-config.php. Zach at Cast Iron Coding forged a path through this jungle, and we’ve come up with a system that allows us all to keep our versions in sync using Git. In the future, I hope to write in a little more techinical detail just how this works.

    We use Git not just in our local development environments, but on the staging and production servers as well. For instance, when a release candidate has been vetted in the staging environment and is ready to go live, it’s extremely easy to launch the version on the production site: I simply shell into the production server and git pull or checkout the tagged version from the repository.