While a subversion repository typically manages many projects, git is a project-centered technology. For each project I create, I want to be able to manage my local repository such that I can push production-ready versions out to remote servers. For example, several client applications are built using the Istarel Workshop frameworks: When a new stable release is ready, I want to be able to easily deploy that new version to client servers to replace the previous framework release.
Create the remote repository
I first need to create a bare repository on the remote server.
markf$ ssh user@www.istarelworkshop.com user$ mkdir -p git/fw && cd git/fw user$ git --bare init Initialized empty Git repository in /path/to/git/fw/ user$ exit
What exactly is a bare repository? Git generally assumes you want a working repository; that is, an active repository where a whole host of change-related activities will be taking place. But that isn't what I want here. I don't need all the history and file detail that a .git directory usually implies. All I need for this remote repository is the repository itself (i.e., the project files in the state as defined by a push to this remote server).
Inform the local repository
Before I can push to that remote repository on www.istarelworkshop.com, I need to modify the configuration file of my local repository. To show the evolution of our local git repository through this process, here is the initial configuration state in my working repository.
markf$ cd ~/Sites/fw markf$ cat .git/config [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true
In order to communicate with that remote repository, the local working repository needs to know about it.
markf$ git remote add workshop ssh://user@www.istarelworkshop.com/path/to/git/fw markf$ cat .git/config [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true [remote "workshop"] url = ssh://user@www.istarelworkshop.com/path/to/git/fw fetch = +refs/heads/*:refs/remotes/workshop/*
Create an ssh key
At this stage, remote push to my www.istarelworkshop.com repository will work just fine. However, it will require me to enter my password every time I do so. To simplify future automation of push-related processes, and to lay the foundation for more secure remote interactions, I will create a private/public ssh key.
I create the keys on my local machine and then copy the public part of the key to the remote machine.
markf$ cd ~/.ssh markf$ ssh-keygen -t dsa
Leave the passphrase empty (you will be prompted twice for the passphrase). At this point, I have created a private (~/.ssh/id_dsa) and public (~/.ssh/id_dsa.pub) key. I want to copy the public key to the remote server.
markf$ ssh user@www.istarelworkshop.com user$ mkdir .ssh && cd .ssh user$ vi authorized_keys
Paste the contents of the local public key into ~/.ssh/authorized_keys on the remote server. Note: if the .ssh directory already exists on the remote server, you may only need to append the contents of ~/.ssh/id_dsa.pub to ~/.ssh/authorized_keys.
Deploy to the remote repository
With the preliminary work complete, I can now push the Istarel Workshop frameworks out to www.istarelworkshop.com.
markf$ git push workshop master
Counting objects: 287, done.
Compressing objects: 100% (279/279), done.
Writing objects: 100% (287/287), 165.89 KiB | 84 KiB/s, done.
Total 287 (delta 68), reused 0 (delta 0)
refs/heads/master: 0000000000000000000000000000000000000000 ->
9da9ef18470740afe623966eec4dbf45cd22788b
To ssh://user@www.istarelworkshop.com/path/to/fw
* [new branch] master -> master
The last step is to clone that remote repository so that the framework files are accessible to applications.
markf$ ssh user@www.istarelworkshop.com user$ git clone git/fw fw
Installing git is always a simple procedure, but with MacPorts, it becomes a one line Unix command. I also want to set some key configuration settings for git.
markf$ sudo port install git-core markf$ git config --global user.name "Mark Fenoglio" markf$ git config --global user.email markf@istarelworkshop.com markf$ git config --global color.diff auto markf$ git config --global color.status auto markf$ git config --global color.branch auto
The first two git config commands set important defaults for identifying the author of changes to the repository. The last three commands are to provide color when git presents information in the console.
Istarel Workshop Frameworks
I intend to eventually migrate all my projects to git, but I want to start with the Istarel Workshop frameworks. This will give me a chance to establish some best practices, particularly with respect to deployment. Before I start, I want to remove all of subversion's hidden directories.
markf$ cd ~/Sites/fw markf$ find . -type d -name '.svn' -print0 | xargs -0 rm -rdf
Now I am ready to establish the git repository for my frameworks.
markf$ git init
Initialized empty Git repository in /Users/markf/Sites/fw/.git/
All that I've done at this point is create an empty working directory. Now I need to populate my local repository with the initial snapshot of the project.
markf$ git add . markf$ git commit
When the commit happens, an intermediate file is opened in your designated editor and you should create the commit message there. When that temporary file is saved, git saves the initial version of my project.
Creating a developer-focused blog application turned out to be a great learning experience. I am generally happy with the design and layout, but there seems to be a glaring omission: the urls to specific articles are not terribly search-engine friendly. So, I want to add a database table that properly maps the public url to the private application module invocation. Clearly, I already have several articles written, for which there is no search-engine friendly url. That means I also need to populate that new database table with appropriate information for existing articles. I can use the patch system to deploy these changes once they are ready.
SQL Patch
The first part of the patch is the creation of a redirect table to handle URL mapping and some hand-coded entries.
Partial Listing: /dblog/shell/sql/2.2.sql
SET client_min_messages = warning; CREATE SEQUENCE redirect_seq START 1; CREATE TABLE redirect ( redirect_id integer default nextval('redirect_seq'), public_path text not null, application_module varchar(96) not null, identifier varchar(4), identifier_value varchar(32) ); ALTER TABLE redirect ADD CONSTRAINT pkey_redirect PRIMARY KEY (redirect_id); INSERT INTO redirect VALUES (DEFAULT, 'dblog', 'BlogViewer', NULL, NULL);
PHP Patch
I have modified the save-related methods in several model objects to add entries to the redirect table when appropriate (i.e., when the applicable entry does not yet exist). I have also updated ApplicationDelegate to handle redirects. With that code written (and which will be updated via subversion as the patch executes), I can now create my PHP patch.
Listing: /dblog/shell/php/2.2.php
<?php
require "constants.php";
// Update redirect with project entries
$select = new DBOMSelect('project');
$projects = DBOMFactory::instanceArray('Project', $select);
foreach ($projects as $project)
{
$project->save();
}
// Update redirect with category entries
$select = new DBOMSelect('category');
$categories = DBOMFactory::instanceArray('Category', $select);
foreach ($categories as $category)
{
$category->save();
}
// Update redirect with article entries
$select = new DBOMSelect('article');
$articles = DBOMFactory::instanceArray('Article', $select);
foreach ($articles as $article)
{
$article->save();
}
?>
Executing the Patch
With the SQL and PHP patch files ready, I can now execute the shell script that applies the patch.
markf$ cd ~/Sites/dblog/shell markf$ sh apply_patch.sh 2.2 Updating framework via subversion... Updating application via subversion... Applying PostgreSQL patch component... Applying PHP patch component...
Another Approach
Notice that in the SQL patch, I directly inserted a record. In many cases, I prefer to instead use the database object architecture. In that case, I would avoid INSERT INTO statements in the SQL patch file and instead create (and save) redirect instances in the PHP patch file.
Imaginary Partial Listing: /dblog/shell/php/2.2.php
<?php
...
// Create specific Redirect entries
$redirect = new Redirect;
$redirect->setPublicPath('dblog')
->setApplicationModule('BlogViewer');
$redirect->save();
?>
One inevitable activity in any web application is updates. I am always refactoring code, adding new features, and improving existing features. I get everything working the way I want in my development environment and then need to deploy the changes. Typically, that deployment has three components: update the scripts via subversion, modify the database, and use php to update database records.
To address this problem, I want to create a lightweight patch system for my applications, and the developerBlog is a good target for experimenting with this idea. Below is a skeleton of the directory strucuture I want.
Directory Listing: /dblog/shell
/dblog/shell
apply_patch.sh
conf/
sh.conf.dist
php/
constants.php.dist
sql/
The patch components will reside in an applicable directory. For example, a patch (that I will arbitrarily name 2.1) has only an SQL component. In that case, there should be a file called 2.1.sql in the sql directory.
I envision a couple configuration files. One (conf/sh.conf) helps apply_patch.sh execute the right versions of PostgreSQL and PHP, and identifies the paths to key directories. The second configuration file (php/constants.php) bootstraps the Istarel Workshop Frameworksto to any PHP script so that I have complete access to the framework and application class hierarchies.
Subversion
Notice that the configuration files both end in .dist. The idea here is that I want to be able to run the patches in my development, staging, or production environment. Clearly, the path and program names and paths are likely to be different in each environment, but I want to have these distributed versions available as guides. Thus, the skeleton as shown is made part of the subversion repository for the project.
markf$ svn add shell markf$ cp shell/conf/sh.conf.dist shell/conf/sh.conf markf$ cp shell/php/constants.php.dist shell/php/constants.php markf$ svn propset svn:ignore sh.conf shell/conf markf$ svn propset svn:ignore constants.php shell/php
In order for the local environment constants to be respected, I make sure that sh.conf and constants.php are not part of the subversion repository.
Patch Application
The name of the patch will be passed as an argument to apply_patch.sh. The shell script then executes a subversion update for the frameworks and the application. If there is an appropriate SQL or PHP patch, those are applied as well.
Listing: /dblog/shell/apply_patch.sh
#!/bin/bash
. conf/sh.conf
# identify the patch being applied
if [ ! $1 ]
then
echo "Patch not defined"
exit
fi
# apply subversion update to framework
echo "Updating framework via subversion..."
cd $FRAMEWORK_DIR
svn up
# apply subversion update to application
echo "Updating application via subversion..."
cd $PROJECT_DIR
svn up
cd shell
# apply database patch
if [ ! -f sql/$1.sql ]; then
echo "No database patch component"
else
echo "Applying PostgreSQL patch component..."
$PSQL -q -U $DATABASE_USER -f sql/$1.sql -d $DATABASE_NAME
fi
# apply PHP patch
if [ ! -f php/$1.php ]; then
echo "No PHP patch component"
else
echo "Applying PHP patch component..."
$PHP $PROJECT_DIR/shell/php/$1.php
fi
This is a bare bones approach to the problem, and future work may require me to chance its structure. For example, I may want to add a switch that tells the script to ignore the step for updating the framework. A subsequent blog entry will demonstrate how to apply an actual patch to the developerBlog.
In Git, each commit is a product of all the modifications and new files added to the working copy. Unlike with Subversion, where all modified files are assumed to be part of the commit, Git requires you to explicitly add them. Fortunately, there is a handy shortcut for adding everything.
markf$ cd ~/Sites/fw markf$ git add . markf$ git status # On branch master # Changes to be committed: # (use "git reset HEAD..." to unstage) # # modified: controller/IWOrderedListController.php # modified: html/IWRequest.php # modified: html/IWURL.php # modified: navigator/IWNavigator.php # modified: test/Autoload.php # new file: test/html/IWRequestTest.php #
When you add modifications and new files in this fashion, Git also respects its own ignore list.
