<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>AdamFranco.com</title>
	<atom:link href="http://www.adamfranco.com/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.adamfranco.com</link>
	<description>Musings, projects, software, and photography.</description>
	<lastBuildDate>Thu, 06 Oct 2011 19:54:25 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.3.1</generator>
		<item>
		<title>Black Bean Crostini</title>
		<link>http://www.adamfranco.com/2011/06/15/black-bean-crostini/</link>
		<comments>http://www.adamfranco.com/2011/06/15/black-bean-crostini/#comments</comments>
		<pubDate>Thu, 16 Jun 2011 02:57:30 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Life and Everything Else]]></category>
		<category><![CDATA[CSA]]></category>
		<category><![CDATA[food]]></category>
		<category><![CDATA[recipes]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=603</guid>
		<description><![CDATA[One of the joys of participating in a CSA is exploring new vegetables and foods that I otherwise wouldn&#8217;t have known about or thought to eat. Last year we received husk cherries, red carrots, bok choy, several types of kale, purple potatoes, and at least 4 varieties of beets in addition to many more standard [...]]]></description>
			<content:encoded><![CDATA[<p><a title="252/365: Tomatos in Sacks by Adam Franco, on Flickr" href="http://www.flickr.com/photos/adamfranco/4976745536/"><img src="http://farm5.static.flickr.com/4126/4976745536_b4c1533be8_m.jpg" alt="252/365: Tomatos in Sacks" align="right" /></a>One of the joys of participating in a <a href="http://www.gildrienfarm.com/about-the-csa.html">CSA</a> is exploring new vegetables and foods that I otherwise wouldn&#8217;t have known about or thought to eat. Last year we received <a href="http://www.flickr.com/photos/adamfranco/4976745536/">husk cherries</a>, <a href="http://www.flickr.com/photos/adamfranco/4982297977/">red carrots</a>, bok choy, several types of kale, purple potatoes, and at least 4 varieties of beets in addition to many more standard vegetable varieties.  I don&#8217;t consider myself sheltered in terms of food, but many of these were simply things I never would have thought to look for even if they are available in a grocery store.</p>
<p><a title="241/365: Red Carrots by Adam Franco, on Flickr" href="http://www.flickr.com/photos/adamfranco/4982297977/"><img style="float: right; clear: right;" src="http://farm5.static.flickr.com/4088/4982297977_443043b36a_m.jpg" alt="241/365: Red Carrots" /></a>For the past two weeks our CSA share from the <a href="http://www.gildrienfarm.com/">Gildrien Farm</a> has included several cups of dried black beans, a food I&#8217;ve eaten many times but never really cooked with. In their weekly letter Jeremy and Caitlin helpfully included <a href="http://www.gildrienfarm.com/2/post/2011/01/puetro-rican-black-beans.html">a recipe for Puerto Rican Black Beans</a>, a tasty-sounding launching pad for the evening&#8217;s dinner.</p>
<p>Since I didn&#8217;t have any bacon grease on hand I figured I would just fry up several large pieces of bacon and use both the meat and the grease. I had planned to make a fritata as the main course for the evening, but after sampling the beans, decided to add some more veggies and put them on bread as our main course. Unfortunately, the result was so delicious that the crostini never made it out of the kitchen for a photo shoot.</p>
<p><strong>Black Bean Crostini</strong></p>
<ul>
<li>1 cup dry black beans, soaked overnight</li>
<li>1 large onion, diced as small as possible</li>
<li>1/2 a red pepper, diced</li>
<li>1/4 lb of bacon (4-5 pieces)</li>
<li>1 cup of cherry tomatoes, quartered</li>
<li>salt</li>
<li>1 baguette</li>
</ul>
<ol>
<li>Soak the beans overnight to soften, then simmer over medium heat for 45 minutes until tender.</li>
<li>While the beans are cooking, fry the bacon in a skillet over medium heat until it is crispy and most of the fat has melted off. Pull the bacon strips out of the pan and let cool, trying to keep as much of the grease in the skillet as possible.</li>
<li>Turn down the heat on the skillet to low. Add the diced onion and some salt to the bacon grease in the skillet and cook for 15 minutes, slowly letting the onion turn clear and caramelize.</li>
<li>Drain the majority of the water from the beans (leaving about a half cup) and add the beans and their water to the skillet with the onion and bacon grease. Stir together with the red pepper. Raise the heat to medium and stew for another 15 minutes or so, until the beans begin to fall apart.</li>
<li>Mash the beans in the skillet with a utensil of some sort until you have chunky bean paste interspersed with red-pepper and bean husks. Crumble the bacon and stir it into the beans. Salt to taste.</li>
<li>Cut the baguette into thin slices. Pile a large dollop of beans on each slice and top with diced cherry tomatoes.</li>
</ol>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2011/06/15/black-bean-crostini/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>River Levels Widget v.1.2.2 available</title>
		<link>http://www.adamfranco.com/2011/03/23/river-levels-widget-v-1-2-2-available/</link>
		<comments>http://www.adamfranco.com/2011/03/23/river-levels-widget-v-1-2-2-available/#comments</comments>
		<pubDate>Thu, 24 Mar 2011 00:46:02 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Computers and Technology]]></category>
		<category><![CDATA[Software]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Dashboard widgets]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=581</guid>
		<description><![CDATA[The RiverLevels widget provides an easy way to monitor the amount of water flowing in your favorite streams and rivers right from your Dashboard. The RiverLevels widget is of particular interest to whitewater kayakers and canoeists. Once any United States Geological Survey (USGS) stream-gauge station is selected, it is automatically refreshed to always provide you [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://downloads.sourceforge.net/waterwidgets/RiverLevels.wdgt-1.2.zip"><img src="http://www.adamfranco.com/files/2008/02/screen-shot.jpg" title="RiverLevels 1.0 Screen Shot" alt="RiverLevels 1.0 Screen Shot" style="width: 100%; max-width: 817px" align="middle" border="0" /></a></p>
<p>The <span class="title">RiverLevels</span> widget provides an easy way to monitor the amount of water flowing in your favorite streams and rivers right from your Dashboard. The <span class="title">RiverLevels</span> widget is of particular interest to whitewater kayakers and canoeists.</p>
<p>Once any United States Geological Survey (USGS) stream-gauge station is selected, it is automatically refreshed to always provide you with the latest graph of the water-level. As of version 1.2 you can choose between two graph styles: discharge in cubic feet per second (CFS) and water-height in feet.</p>
<p>This widget is Free software, licensed under the GNU General Public License (GPL) version 3 or later.</p>
<ul>
<li><strong><a href="http://sourceforge.net/projects/waterwidgets/files/RiverLevels.wdgt/1.2.2/RiverLevels.wdgt-1.2.2.zip/download">Download</a></strong> (<a href="http://sourceforge.net/projects/waterwidgets/files/RiverLevels.wdgt/1.2.2/">alternate download link</a>)</li>
</ul>
<ul>
<li><a href="http://waterwidgets.sourceforge.net/">More Info</a></li>
<li><a href="http://www.apple.com/downloads/dashboard/information/riverlevels.html">Apple&#8217;s Download page for RiverLevels.wdgt</a></li>
<li><a href="http://sourceforge.net/tracker/?group_id=149897&amp;atid=776122">Bug Tracker</a></li>
</ul>
<p><strong> Requirements:</strong></p>
<ul>
<li>OS X &#8211; 10.4 &#8220;Tiger&#8221; or later</li>
</ul>
<p><strong>Change Log:</strong><br />
1.2.2 (2011-03-23)</p>
<ul>
<li>Fix for image URL change in USGS site.</li>
</ul>
<p><a href="http://www.adamfranco.com/2008/02/10/river-levels-widget-v121/">1.2.1 (2008-02-10)</a></p>
<ul>
<li>New zip archive includes the &#8216;library&#8217; directory missing in the 1.2 release.</li>
</ul>
<p><a href="http://www.adamfranco.com/2008/02/07/river-levels-widget-v12/">1.2 (2008-02-06)</a></p>
<ul>
<li>Fixed Leopard (10.5) compatability bug.</li>
<li> Added the ability to choose Gauge Height (ft) in addition to discharge (CFS).</li>
</ul>
<p>1.1 (2007-01-08)</p>
<ul>
<li>Fixed graphs extending off bottom of widget</li>
<li>Fixed invisibility of front refresh icon</li>
</ul>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2011/03/23/river-levels-widget-v-1-2-2-available/feed/</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>Mission-Style Bookshelf</title>
		<link>http://www.adamfranco.com/2011/02/12/mission-style-bookshelf/</link>
		<comments>http://www.adamfranco.com/2011/02/12/mission-style-bookshelf/#comments</comments>
		<pubDate>Sun, 13 Feb 2011 02:13:05 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Life and Everything Else]]></category>
		<category><![CDATA[projects]]></category>
		<category><![CDATA[woodworking]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=572</guid>
		<description><![CDATA[My current woodworking project is a Mission-style bookshelf that I designed to match the sofa table that I built last year. The bookshelf will sit below a window to the kitchen, so it is low and extra wide to fit that space. To support the weight of the books without sagging, sets of stiles transfers [...]]]></description>
			<content:encoded><![CDATA[<p><object width="600" height="450"><param name="flashvars" value="offsite=true&#038;lang=en-us&#038;page_show_url=%2Fphotos%2Fadamfranco%2Fsets%2F72157626035422760%2Fshow%2F&#038;page_show_back_url=%2Fphotos%2Fadamfranco%2Fsets%2F72157626035422760%2F&#038;set_id=72157626035422760&#038;jump_to="></param><param name="movie" value="http://www.flickr.com/apps/slideshow/show.swf?v=71649"></param><param name="allowFullScreen" value="true"></param><embed type="application/x-shockwave-flash" src="http://www.flickr.com/apps/slideshow/show.swf?v=71649" allowFullScreen="true" flashvars="offsite=true&#038;lang=en-us&#038;page_show_url=%2Fphotos%2Fadamfranco%2Fsets%2F72157626035422760%2Fshow%2F&#038;page_show_back_url=%2Fphotos%2Fadamfranco%2Fsets%2F72157626035422760%2F&#038;set_id=72157626035422760&#038;jump_to=" width="600" height="450"></embed></object></p>
<p>My current woodworking project is a Mission-style bookshelf that I designed to match the <a href="http://www.adamfranco.com/2010/04/11/sofa-table-complete/">sofa table</a> that I built last year. The bookshelf will sit below a window to the kitchen, so it is low and extra wide to fit that space. To support the weight of the books without sagging, sets of stiles transfers weight from the middle shelf to the frame above and below.</p>
<p>I am building the bookshelf out of cherry. Like the sofa table, all joinery is mortise and tenon. This time I am squaring out the mortises with a new set of mortising chisels rather than rounding off the tenons with a knife as I did on the sofa table &#8212; which is making the process go much faster.</p>
<p>If you like the design and wish to build one for yourself, you can download my <a href='http://www.adamfranco.com/files/2011/02/Mission-Bookshelf.skp'>SketchUp model</a> as a starting point.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2011/02/12/mission-style-bookshelf/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Git Tip: Grouping feature-branch commits when merging.</title>
		<link>http://www.adamfranco.com/2010/12/12/git-tip-grouping-feature-branch-commits-when-merging/</link>
		<comments>http://www.adamfranco.com/2010/12/12/git-tip-grouping-feature-branch-commits-when-merging/#comments</comments>
		<pubDate>Sun, 12 Dec 2010 18:32:02 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Computers and Technology]]></category>
		<category><![CDATA[Work/Professional]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[source-control]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=478</guid>
		<description><![CDATA[Let&#8217;s say you are working on a large feature or update that requires a bunch of commits to complete. You finish up with your work and are then ready to merge it onto your master branch. For example, here is the history of my drupal repository after some work updating the cas module to the [...]]]></description>
			<content:encoded><![CDATA[<p>Let&#8217;s say you are working on a large feature or update that requires a bunch of commits to complete. You finish up with your work and are then ready to merge it onto your master branch.</p>
<p>For example, here is the history of my drupal repository after some work updating the cas module to the latest version (and to support the new version of <a href="https://wiki.jasig.org/display/CASC/phpCAS">phpCAS</a>):<br />
<a href="http://www.adamfranco.com/files/2010/12/git-merge-0.png"><img class="aligncenter size-full wp-image-479" title="git-merge-0" src="http://www.adamfranco.com/files/2010/12/git-merge-0.png" alt="" width="100%" /></a></p>
<p>As you can see, I have a number of commits, followed by a merge in with the new module code, followed by some more commits.</p>
<p>Now, if I merge my feature branch (<code>master-cas3-simple</code>) into the <code>master</code> via</p>
<pre>git merge  master-cas3-simple</pre>
<p>then the history will look like this:<br />
<a href="http://www.adamfranco.com/files/2010/12/git-merge-ff.png"><img class="aligncenter size-full wp-image-482" title="git-merge-ff" src="http://www.adamfranco.com/files/2010/12/git-merge-ff.png" alt="" width="100%" /></a></p>
<p>While the history is all there, it isn&#8217;t obvious that all of the commits beyond &#8220;Convert MS Word quote&#8230;&#8221; are a single unit of work. They all kind of blend together because git performed a &#8220;fast-forward&#8221; commit. Usually fast-forward commits are helpful since they keep the history from being cluttered with hundreds of unnecessary merge commits, but in this case we are loosing the context of these commits being a unit of work.</p>
<p>To preserve the grouping of these commits together I can instead force the merge operation to create a merge commit (and even append a message) by using the <code>--no-ff</code> option to <code>git merge</code>:</p>
<pre>git merge --no-ff -m "Upgraded CAS support to to cas-6.x-3.x-dev and phpCAS 1.2.0 RC2.5" master-cas3-simple</pre>
<p>This results in the history below:<br />
<a href="http://www.adamfranco.com/files/2010/12/git-merge-no-ff.png"><img class="aligncenter size-full wp-image-484" title="git-merge-no-ff" src="http://www.adamfranco.com/files/2010/12/git-merge-no-ff.png" alt="" width="100%" /></a></p>
<p>As you can see, merging with the <code>--no-ff</code> option creates a merge commit which very obviously delineates work on this feature. If we decided that we wanted to roll back this feature it would be much easier to sort out where the starting point before the feature was.</p>
<div class='attribution'>
Thanks to Vincent Driessen for turning me onto the utility of the the <code>--no-ff</code> merge option via his post &#8220;<a href="http://nvie.com/posts/a-successful-git-branching-model/">A successful Git branching model</a>&#8220;.</div>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2010/12/12/git-tip-grouping-feature-branch-commits-when-merging/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Mirroring a Subversion repository on Github</title>
		<link>http://www.adamfranco.com/2010/12/05/mirroring-a-subversion-repository-on-github/</link>
		<comments>http://www.adamfranco.com/2010/12/05/mirroring-a-subversion-repository-on-github/#comments</comments>
		<pubDate>Sun, 05 Dec 2010 05:30:12 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Computers and Technology]]></category>
		<category><![CDATA[Work/Professional]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[git-svn]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[source-control]]></category>
		<category><![CDATA[Subversion]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=445</guid>
		<description><![CDATA[For the past few months I have been doing a lot of work on the phpCAS library, mostly to improve the community trunk of phpCAS so that I wouldn&#8217;t have to maintain our own custom fork with support for the CAS attribute format we use at Middlebury College. The phpCAS project lead, Joachim Fritschi, has [...]]]></description>
			<content:encoded><![CDATA[<p>For the past few months I have been doing a lot of <a href="http://www.ohloh.net/p/phpcas/contributors/290013371731193">work</a> on the <a href="https://wiki.jasig.org/display/CASC/phpCAS">phpCAS library</a>, mostly to improve the community trunk of phpCAS so that I wouldn&#8217;t have to maintain our own custom fork with support for the <a href="https://issues.jasig.org/browse/PHPCAS-88">CAS attribute</a> format we use at Middlebury College. The phpCAS project lead, Joachim Fritschi, has been great to work with and I&#8217;ve had a blast helping out with the project.</p>
<p>The tooling has involved a few challenges however, since <a href="http://www.jasig.org/">Jasig</a> (the organization that hosts the <a href="http://www.jasig.org/cas">CAS</a> and phpCAS projects) uses <a href="http://subversion.apache.org/">Subversion</a> for its source-code repositories and we use <a href="http://git-scm.com/">Git</a> for all of our projects. Now, I could just suck it up and use Subversion when doing phpCAS development, but there are a few reasons I don&#8217;t:</p>
<ol>
<li>We make use of <a href="http://www.kernel.org/pub/software/scm/git/docs/user-manual.html#submodules">Git submodules</a> to include phpCAS along with the source-code of our applications, necessitating the use of a public Git repository that includes phpCAS.</li>
<li>The <a href="http://www.kernel.org/pub/software/scm/git/docs/git-svn.html">git-svn</a> tools allow me to use git on my end to work with a Subversion repository, which is great because&#8230;</li>
<li>I find that Git&#8217;s fast history browsing and searching make troubleshooting and bug fixing much easier than any other tools I&#8217;ve used.</li>
</ol>
<p>For the past two years I have been using git-svn to work with the phpCAS repository and every so often pushing changes up to a <a href="https://github.com/adamfranco/phpcas/">public Git repository on GitHub</a>. Our applications reference this repository as a submodule when they need to make use of phpCAS. Now that I&#8217;ve been doing more work on phpCAS (and am more interested in keeping our applications using up-to-date versions), I&#8217;ve decided to automate the process of mirroring the Subversion repository on GitHub. Read on for details of how I&#8217;ve set this up and the scripts for keeping the mirror in sync.</p>
<p><span id="more-445"></span></p>
<p>On my development server I have a git repository I&#8217;ve cloned from the Jasig Subversion repository via:</p>
<pre>git svn clone --stdlayout https://source.jasig.org/cas-clients/phpcas/</pre>
<p>I use this repository for my phpCAS development and am continually using <code>git svn rebase</code> and <code>git svn dcommit</code> to update my branches from Subversion and commit changes back to the Subversion repository.</p>
<p>My goal was to fetch from Subversion and push all of the branches and tags from the Subversion repository to GitHub while ignoring any private branches or un-dcommited changes I might have kicking about my development repository.</p>
<p><strong>Step 1: Add the GitHub repository as a remote</strong></p>
<pre>git remote add github git@github.com:adamfranco/phpcas.git</pre>
<p><strong>Step 2: Fetch the latest changes from the svn repository</strong><br />
To do this, I just needed to run <code>git svn fetch</code> to import commits from the Subversion repository into my Git repository.</p>
<p><strong>Step 3: Make Git tags for Subversion tag-branches</strong><br />
I may be doing something wrong, but it seems that Subversion tags come through <code>git svn</code> as git branches rather than as git &#8220;tag&#8221; objects. Basically they are a branch with a single commit that just adds the tag message, but no content change. Using <code>git show</code> I found I could grab the parent id, message, and other metadata from the &#8220;tag-branch&#8221;, then feed that into <code>git tag</code> to create actual tag objects in the git repository.</p>
<p style="text-align: center;"><a href="http://www.adamfranco.com/files/2010/12/git-svn_tags.png"><img class="aligncenter " title="git-svn_tags" src="http://www.adamfranco.com/files/2010/12/git-svn_tags.png" alt="" width="100%" /></a></p>
<p><strong>Step 4: Push subversion branches and newly created tags to GitHub</strong><br />
When called with no parameters <code>git push</code> will push all branches that have matching names in both the source and the destination repository. This wasn&#8217;t going to work for me since I want to only automatically push the branch-state that exists in subversion (not any un-dcommitted changes in my Git repository) and want to create mirrors of any new branches that appear in the Subversion repository. To accomplish this I needed to specify every branch individually. I determined the list of branches via:</p>
<pre>git branch -r | grep -v '/' | grep -v trunk</pre>
<p>then looped through them and appended them to the hard-coded mapping between the svn &#8220;trunk&#8221; and the GitHub &#8220;master&#8221;:</p>
<pre>$cmd = 'git push --tags github refs/remotes/trunk:refs/heads/master ';
foreach ($svnBranches as $branch) {
	$cmd .= 'refs/remotes/'.$branch.':refs/heads/'.$branch.' ';
}</pre>
<p><strong>All together: <code>update_github_phpcas</code></strong><br />
Below is a script which performs the tasks above. I&#8217;ve added it to my crontab so that it runs every half-hour and keeps my GitHub repository in-sync with the Jasig Subversion repository.</p>
<div style="display: block; border: 1px dotted; padding: 5px;"><code><span style="color: #000000;"><br />
#!/usr/local/bin/php<br />
<span style="color: #0000bb;">&lt;?php<br />
</span><span style="color: #ff8000;">/**<br />
* Script to mirror a Subversion repository on GitHub or another public Git repository.<br />
*<br />
* Author: Adam Franco (afranco@middlebury.edu)<br />
* Date: 2010-12-04<br />
* License: GNU General Public License (GPL) version 2 or later.<br />
*/&nbsp;</p>
<p><span style="color: #0000bb;">chdir</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'/home/afranco/private_html/phpcas/'</span><span style="color: #007700;">);</span></p>
<p><span style="color: #ff8000;">// Fetch from svn.<br />
</span><span style="color: #007700;">`</span><span style="color: #0000bb;">git svn fetch</span><span style="color: #007700;">`;</span></p>
<p><span style="color: #ff8000;">// Lookup all of the svn branches<br />
</span><span style="color: #0000bb;">$svnBranches </span><span style="color: #007700;">= </span><span style="color: #0000bb;">explode</span><span style="color: #007700;">(</span><span style="color: #dd0000;">"\n"</span><span style="color: #007700;">, </span><span style="color: #0000bb;">trim</span><span style="color: #007700;">(`</span><span style="color: #0000bb;">git branch -r | grep -v '/' | grep -v trunk</span><span style="color: #007700;">`));<br />
</span><span style="color: #0000bb;">$svnBranches </span><span style="color: #007700;">= </span><span style="color: #0000bb;">array_map</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'trim'</span><span style="color: #007700;">, </span><span style="color: #0000bb;">$svnBranches</span><span style="color: #007700;">);</span></p>
<p><span style="color: #ff8000;">// Add all of our branches to the list of those to push<br />
</span><span style="color: #0000bb;">$cmd </span><span style="color: #007700;">= </span><span style="color: #dd0000;">'git push --tags github refs/remotes/trunk:refs/heads/master '</span><span style="color: #007700;">;<br />
foreach (</span><span style="color: #0000bb;">$svnBranches </span><span style="color: #007700;">as </span><span style="color: #0000bb;">$branch</span><span style="color: #007700;">) {<br />
</span><span style="color: #0000bb;">$cmd </span><span style="color: #007700;">.= </span><span style="color: #dd0000;">'refs/remotes/'</span><span style="color: #007700;">.</span><span style="color: #0000bb;">$branch</span><span style="color: #007700;">.</span><span style="color: #dd0000;">':refs/heads/'</span><span style="color: #007700;">.</span><span style="color: #0000bb;">$branch</span><span style="color: #007700;">.</span><span style="color: #dd0000;">' '</span><span style="color: #007700;">;<br />
}</span></p>
<p><span style="color: #ff8000;">// Ensure that Git tags are created for every SVN tag branch.<br />
</span><span style="color: #0000bb;">$svnBranches </span><span style="color: #007700;">= </span><span style="color: #0000bb;">explode</span><span style="color: #007700;">(</span><span style="color: #dd0000;">"\n"</span><span style="color: #007700;">, </span><span style="color: #0000bb;">trim</span><span style="color: #007700;">(`</span><span style="color: #0000bb;">git branch -r | grep 'tags/'</span><span style="color: #007700;">`));<br />
</span><span style="color: #0000bb;">$svnBranches </span><span style="color: #007700;">= </span><span style="color: #0000bb;">array_map</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'trim'</span><span style="color: #007700;">, </span><span style="color: #0000bb;">$svnBranches</span><span style="color: #007700;">);<br />
foreach (</span><span style="color: #0000bb;">$svnBranches </span><span style="color: #007700;">as </span><span style="color: #0000bb;">$svnTag</span><span style="color: #007700;">) {<br />
</span><span style="color: #0000bb;">$ref </span><span style="color: #007700;">= </span><span style="color: #dd0000;">"refs/remotes/$svnTag"</span><span style="color: #007700;">;<br />
</span><span style="color: #0000bb;">$parent </span><span style="color: #007700;">= </span><span style="color: #0000bb;">shell_exec</span><span style="color: #007700;">(</span><span style="color: #dd0000;">"git show --format=\"format:%P\" $ref"</span><span style="color: #007700;">);</span></p>
<p><span style="color: #ff8000;">// If there are no tags on the parent of the tag branch, add one.<br />
</span><span style="color: #007700;">if (!</span><span style="color: #0000bb;">strlen</span><span style="color: #007700;">(</span><span style="color: #0000bb;">trim</span><span style="color: #007700;">(`</span><span style="color: #0000bb;">git tag --contains $parent</span><span style="color: #007700;">`))) {<br />
</span><span style="color: #0000bb;">$message </span><span style="color: #007700;">= </span><span style="color: #0000bb;">shell_exec</span><span style="color: #007700;">(</span><span style="color: #dd0000;">"git show --format=\"format:%s%ntagged by %aN on %aD\" $ref"</span><span style="color: #007700;">);<br />
</span><span style="color: #0000bb;">$date </span><span style="color: #007700;">= </span><span style="color: #0000bb;">shell_exec</span><span style="color: #007700;">(</span><span style="color: #dd0000;">"git show --format=\"format:%ai\" $ref"</span><span style="color: #007700;">);<br />
</span><span style="color: #0000bb;">$tagName </span><span style="color: #007700;">= </span><span style="color: #0000bb;">str_replace</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'tags/'</span><span style="color: #007700;">, </span><span style="color: #dd0000;">''</span><span style="color: #007700;">, </span><span style="color: #0000bb;">$svnTag</span><span style="color: #007700;">);</span></p>
<p><span style="color: #0000bb;">$tagCmd </span><span style="color: #007700;">= </span><span style="color: #dd0000;">'GIT_COMMITTER_DATE="'</span><span style="color: #007700;">.</span><span style="color: #0000bb;">$date</span><span style="color: #007700;">.</span><span style="color: #dd0000;">'" git tag -a -m "'</span><span style="color: #007700;">.</span><span style="color: #0000bb;">$message</span><span style="color: #007700;">.</span><span style="color: #dd0000;">'" '</span><span style="color: #007700;">.</span><span style="color: #0000bb;">$tagName</span><span style="color: #007700;">.</span><span style="color: #dd0000;">' '</span><span style="color: #007700;">.</span><span style="color: #0000bb;">$parent</span><span style="color: #007700;">;<br />
</span><span style="color: #ff8000;">#        print $tagCmd ."\n";<br />
#        print "Creating tag $tagName\n";<br />
</span><span style="color: #007700;">`</span><span style="color: #0000bb;">$tagCmd</span><span style="color: #007700;">`;<br />
}<br />
}</span></p>
<p><span style="color: #ff8000;">#print $cmd;<br />
#print "\n";</span></p>
<p><span style="color: #0000bb;">$output </span><span style="color: #007700;">= `</span><span style="color: #0000bb;">$cmd 2&gt;&amp;1</span><span style="color: #007700;">`;</span></p>
<p>if (<span style="color: #0000bb;">trim</span><span style="color: #007700;">(</span><span style="color: #0000bb;">$output</span><span style="color: #007700;">) != </span><span style="color: #dd0000;">'Everything up-to-date'</span><span style="color: #007700;">)<br />
print </span><span style="color: #0000bb;">$output</span><span style="color: #007700;">.</span><span style="color: #dd0000;">"\n"</span><span style="color: #007700;">;</span></p>
<p></span></span></code>&nbsp;</p>
<p><code> </code>&nbsp;</p>
</div>
<p>I think that this script should work with very few changes for mirroring any Subversion repository as a Git repository.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2010/12/05/mirroring-a-subversion-repository-on-github/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Bittersweet Falls</title>
		<link>http://www.adamfranco.com/2010/11/07/bittersweet-falls/</link>
		<comments>http://www.adamfranco.com/2010/11/07/bittersweet-falls/#comments</comments>
		<pubDate>Mon, 08 Nov 2010 03:09:09 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Life and Everything Else]]></category>
		<category><![CDATA[hiking]]></category>
		<category><![CDATA[outdoors]]></category>
		<category><![CDATA[photography]]></category>
		<category><![CDATA[Vermont]]></category>
		<category><![CDATA[waterfalls]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=435</guid>
		<description><![CDATA[A hundred yards from the aptly-named Bittersweet Falls Road, the Beaver Brook cascades over a formation of marble and dolomite to create a beautiful 18 foot cascade. Above Bittersweet Falls, the Beaver Brook cuts a deep ravine through layers of black slate. The gorge can be difficult to traverse without getting one&#8217;s feet wet, but [...]]]></description>
			<content:encoded><![CDATA[<p>A hundred yards from the aptly-named Bittersweet Falls Road, the Beaver Brook cascades over a formation of marble and dolomite to create a beautiful 18 foot cascade.</p>
<p><a href="http://www.flickr.com/photos/adamfranco/5156790398/" title="Bittersweet Falls by Adam Franco, on Flickr"><img src="http://farm5.static.flickr.com/4003/5156790398_292332eda6.jpg" width="500" height="333" alt="Bittersweet Falls" /></a></p>
<p>Above Bittersweet Falls, the Beaver Brook cuts a deep ravine through layers of black slate. The gorge can be difficult to traverse without getting one&#8217;s feet wet, but is filled with cascades and mossy bottoms ringed by ferns and overshadowed by hemlocks.</p>
<p><a href="http://www.flickr.com/photos/adamfranco/5156180895/" title="In the glen by Adam Franco, on Flickr"><img src="http://farm5.static.flickr.com/4124/5156180895_03c0ec8e7b.jpg" width="333" height="500" alt="In the glen" /></a></p>
<p>I headed out the door today planing to swing by Bittersweet Falls for a few quick shots before driving out to the Dead Creek Wildlife Management Area where stargazer05756 has been <a href="http://www.flickr.com/photos/star05/5154438406/">following the migration of snow geese</a>. I never made it to Dead Creek. After taking a few shots below the falls I climbed up above and noticed an impressive gorge winding upstream. Ever since I was a child I have always loved exploring up cascading streams. There is just something magical above clambering around a rock to find another waterfall or quiet pool surrounded by moss, ferns, hemlocks &#8212; and in the south, rhododendrons.</p>
<p><a href="http://www.flickr.com/photos/adamfranco/5156791674/" title="311/365: In the glen by Adam Franco, on Flickr"><img src="http://farm2.static.flickr.com/1249/5156791674_2e6868b936.jpg" width="443" height="500" alt="311/365: In the glen" /></a></p>
<p>The Beaver Brook didn&#8217;t disappoint and while its steep slate side posed a challenge, I hiked about a third of a mile upstream along the stream bed before scaling the hillside and quickly walking back to the car from above.</p>
<p><iframe width="550" height="350" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" src="http://maps.google.com/maps?f=q&amp;source=s_q&amp;hl=en&amp;geocode=&amp;q=http:%2F%2Fwww.adamfranco.com%2Ffiles%2F2010%2F11%2FBittersweet-Falls-Combined.kml&amp;sll=44.015337,-73.16734&amp;sspn=0.132586,0.277748&amp;ie=UTF8&amp;t=h&amp;ll=44.028664,-73.215723&amp;spn=0.0054,0.011802&amp;z=16&amp;output=embed"></iframe><br /><small><a href="http://maps.google.com/maps?f=q&amp;source=embed&amp;hl=en&amp;geocode=&amp;q=http:%2F%2Fwww.adamfranco.com%2Ffiles%2F2010%2F11%2FBittersweet-Falls-Combined.kml&amp;sll=44.015337,-73.16734&amp;sspn=0.132586,0.277748&amp;ie=UTF8&amp;t=h&amp;ll=44.028664,-73.215723&amp;spn=0.0054,0.011802&amp;z=16" style="color:#0000FF;text-align:left">View Larger Map</a></small></p>
<p>The <a href="http://www.northeastwaterfalls.com/waterfall.php?num=11&#038;p=0">Northeast Waterfalls site</a> has directions and more info.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2010/11/07/bittersweet-falls/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>BASH tip: Top web pages</title>
		<link>http://www.adamfranco.com/2010/10/14/bash-tip-top-web-pages/</link>
		<comments>http://www.adamfranco.com/2010/10/14/bash-tip-top-web-pages/#comments</comments>
		<pubDate>Thu, 14 Oct 2010 16:00:04 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Computers and Technology]]></category>
		<category><![CDATA[Work/Professional]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[BASH]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[web-development]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=431</guid>
		<description><![CDATA[Here is a quick command to generate a list of the top pages in the Apache web-server&#8217;s access log: gawk '{ print $7}' /var/log/httpd/access_log &#124; sort &#124; uniq -c &#124; sort -nr &#124; head -n 20 Parts of the command explained: gawk '{ print $7}' &#8212; return only the 7th [white-space delimited] column of text [...]]]></description>
			<content:encoded><![CDATA[<p>Here is a quick command to generate a list of the top pages in the Apache web-server&#8217;s access log:</p>
<p><code>gawk '{ print $7}' /var/log/httpd/access_log | sort | uniq -c | sort -nr | head -n 20</code></p>
<p>Parts of the command explained:</p>
<ol>
<li><code>gawk '{ print $7}' </code> &#8212; return only the 7th [white-space delimited] column of text from the access log, which happens to be the path requested.</li>
<li><code>sort </code> &#8212; sort the lines of the output.</li>
<li><code>uniq -c </code> &#8212; condense the output to unique lines, prepending each line with the number of times that line occurs.</li>
<li><code>sort -nr </code>&#8211; sort the resulting lines numerically in reverse order.</li>
<li><code>head -n 20 </code> &#8212; chop off all but the first 20 lines.</li>
</ol>
<p>The result should look something like this:</p>
<pre>  83361 /
  49582 /feed
  39616 /robots.txt
  36265 /favicon.ico
  17048 /?feed=rss2
  10798 /archives/3
  10036 /wp-content/uploads/2007/05/img_7870_header.jpg
   9913 /wp-includes/images/smilies/icon_smile.gif
   9425 /wp-comments-post.php
   8274 /feed/
   7508 /archives/category/work/feed
   7367 /archives/88
   7312 /photos/10_small/IMG_3023.JPG.jpg
   7175 /photos/10_small/IMG_3028.JPG.jpg
   7151 /photos/10_small/IMG_3024.JPG.jpg
   7096 /photos/10_small/IMG_3026.JPG.jpg
   6381 /photosetToKML.php?set=72157594417350372&#038;size=small
   6253 /qtvr/2007-04-05_back_deck_snow%20-%2010000x5000%20-%20SLIN%20-%20Blended%20Layer0002.jpg
   5798 /photosetToKML.php
   4344 /archives/category/photography</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2010/10/14/bash-tip-top-web-pages/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Adding reverse-proxy caching to PHP applications</title>
		<link>http://www.adamfranco.com/2010/06/14/adding-reverse-proxy-caching-to-php-applications/</link>
		<comments>http://www.adamfranco.com/2010/06/14/adding-reverse-proxy-caching-to-php-applications/#comments</comments>
		<pubDate>Mon, 14 Jun 2010 16:03:57 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Computers and Technology]]></category>
		<category><![CDATA[Work/Professional]]></category>
		<category><![CDATA[caching]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[reverse-proxy]]></category>
		<category><![CDATA[Varnish]]></category>
		<category><![CDATA[web-development]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=426</guid>
		<description><![CDATA[Note: This is a cross-post of documentation I am writing about Lazy Sessions. Why use reverse-proxy caching? For most public-facing web applications, the significant majority of their traffic is anonymous, non-authenticated users. Even with a variety of internal data-cache mechanisms and other good optimizations, a large amount of code execution goes into executing a PHP [...]]]></description>
			<content:encoded><![CDATA[<p><em>Note: This is a cross-post of <a href="http://wiki.github.com/adamfranco/lazy_sessions/adding-reverse-proxy-caching-to-php-applications">documentation I am writing about Lazy Sessions</a>.</em></p>
<h1>Why use reverse-proxy caching?</h1>
<p>For most public-facing web applications, the significant majority of their traffic is anonymous, non-authenticated users. Even with a variety of internal data-cache mechanisms and other good optimizations, a large amount of code execution goes into executing a <span class="caps">PHP</span> application to generate a page even if the content of this page will be the same for many users. Code and query optimization are very important to improving the experience for all users of a web application, but even the most basic &ldquo;Hello World&rdquo; script will top out at about 3k requests/second due to the overhead of Apache and <span class="caps">PHP</span> &mdash; many real applications top out at less than 200 requests/second. Varnish, a light-weight proxy-server that can run on the same host as the webserver, can cache pages in memory and can serve them at rates of more than 10k requests/second with thousands of concurrent connections.</p>
<p>While the point of web-applications is to have content be dynamic and easily changeable, for most applications and most of the anonymous users, receiving content that is slightly stale (cached for 5 minutes or something similar) isn&rsquo;t a big deal. Sure, visitors to your blog might not see the latest post for a few minutes, but they will get their response in 4 milliseconds rather than 2 seconds.</p>
<p>Should your site get posted on Slashdot, a caching reverse-proxy server will give anonymous visitor #2 and up the same page from cache (until expiration), while authenticated users continue to have their requests passed through to the Apache/<span class="caps">PHP</span> back-end. Everyone wins.</p>
<p><span id="more-426"></span></p>
<h1>Caveats</h1>
<p>Before we get into how to set this up, you should be aware of a few caveats (in addition to increased complexity) that come with this scheme.</p>
<h2>1. Stale Content</h2>
<p>Ideally, pages would always be served from the cache for as long as they don&rsquo;t change, then the application would expire pages when they are changed on the back-end. Varnish has an <span class="caps">API</span> that supports this behavior and <a href="http://drupal.org/project/Varnish">Drupal Varnish module</a> is being developed to do this dynamic cache-clearing for Drupal sites, but overall, dynamic cache clearing is much more difficult to set up than time-based cache expiration.</p>
<p>When using time-based cache expiration, the challenge is to balance the needs for content freshness (shorter cache lifetimes) against the efficiency of cache hits (longer cache lifetimes will result in more clients using the cached versions). For content that doesn&rsquo;t need to be up-to-the-minute fresh, a cache lifetime of around 5 minutes might be a good starting point. If the content only changes daily at certain time, a fixed expiration time (shortly after the data sync) might be appropriate.</p>
<h2>2. Cookie Use</h2>
<p>If your application only uses a cookies set by PHP&rsquo;s <code>session_start()</code> function, then <code>lazy_sessions.php</code> should work transparently without modification of either that include file or your application (other than including the file). If your application sets other cookies then these will cause the reverse-proxy not to cache them unless you specifically exclude them in the reverse-proxy server&rsquo;s configuration.</p>
<h2>3. Data Caching in the <code>$_SESSION</code></h2>
<p>If you use the <code>$_SESSION</code> array as a data cache on anonymous requests, then these anonymous requests will be given a session cookie and their requests won&rsquo;t be served from the reverse-proxy&rsquo;s cache. Rather than using the <code>$_SESSION</code> array for non-user-specific data, cache such data with <span class="caps">APC</span> or memcached. This also has the advantage of such non-user-specific data not having to be rebuilt for every new client.</p>
<h2>4. <code>flush()</code> and output buffering</h2>
<p>The default <span class="caps">PHP</span> session handling mechanism adds the session cookie to the response headers right when <code>session_start()</code> is called and writes the data off to the file-system after the script exits and the data has been sent. This default behavior ensures that users will always get a session cookie and saves the session data as the final processing step after all class destructors have been called.</p>
<p>Since we don&rsquo;t want to always set a session cookie, we need to remove the <code>Set-Cookie</code> header before headers are sent to the client. Output buffering with <code>ob_start()</code> will ensure that we have a chance to decide to clear the <code>Set-Cookie</code> header at script shutdown.</p>
<p>In some cases (such as incrementally sending large binary files) we want to send the content body (and therefor also the headers) before the script exits using the <code>flush()</code> function. To ensure that the session cookie is properly removed <code>session_write_close()</code> must be called before <code>flush()</code> or any other code that causes headers to be sent.</p>
<h1>Implementation</h1>
<p>Implementing reverse-proxy caching has three steps: <span class="caps">PHP</span> changes to enable lazy sessions, <span class="caps">PHP</span> changes to set cache-controlling headers, and finally the reverse-proxy server setup. For this example I&rsquo;ll use the Varnish reverse-proxy server, but others could be used instead.</p>
<h2>1. <span class="caps">PHP</span>: Lazy Sessions</h2>
<p>The first thing that needs to happen to make anonymous requests cache-able in an application that uses sessions is to ensure that sessions are only started when there is session data to be stored. By default, PHP&rsquo;s session handling mechanisms add a session cookie to the response header and store a session data file on the server on page-load that calls <code>session_start()</code>. While this behavior makes it easy to write applications that use sessions, it effectively means that there is no way to differentiate between responses that are for a particular user and those that could be for many users.</p>
<p>Including the <a href="http://github.com/adamfranco/lazy_sessions/blob/master/lazy_sessions.php"><code>lazy_sessions.php</code> file</a> before <code>session_start()</code> is called will override the default session-handling mechanism with one that checks to see if there is any data in the <code>$_SESSION</code> array before sending the user a <code>Set-Cookie</code> header and storing a session file:</p>
<pre>
<code>&lt;?php

// Include files or other pre-session_start code

require_once('lazy_sessions/lazy_sessions.php');
start_session();

// The rest of the application code.
?&gt;
</code>
</pre>
<p>If your application needs to flush content and thereby send headers before script shutdown (such as incrementally sending file data), call <code>session_write_close()</code> if <code>session_start()</code> has been called for that script:</p>
<pre>
<code>&lt;?php

// Include files or other pre-session_start code

require_once('lazy_sessions/lazy_sessions.php');
start_session();

// other application code.

// If session_write_close() is not called before flushing, then the Set-Cookie
// header will be sent before our custom session handler has a chance to determine
// if a session is even needed.
session_write_close();

print "Hello";
flush();
print " World.";
flush();

?&gt;
</code>
</pre>
<h2>2. <span class="caps">PHP</span>: Cache-Control headers</h2>
<p>Now that we have our cookies straightened out, we need to ensure that our <span class="caps">PHP</span> scripts respond with <span class="caps">HTTP</span> headers that indicate that downstream clients such as our reverse-proxy and the user&rsquo;s browser are allowed to cache anonymous pages. There are a number of different <a href="http://wiki.github.com/adamfranco/lazy_sessions/cache-controlling-headers">Cache-Controlling Headers</a> that may affect whether a particular cache may store a given response. By default, <span class="caps">PHP</span> sets all of these headers to indicate that no caches may store any pages, ensuring that they are dynamic.</p>
<pre>
<code>&lt;?php

// If the session data is empty, then we could assume that there is no per-user data
// and that the response can be cached.
if (!count($_SESSION)) {

// Alternatively, we could check an application-specific value (such as a user-id)
// to determine if the response is for a particular user.
// if (!isset($_SESSION['user_id'])) {

// Cache for 5 minutes
$maxAge = 300;

header('Expires: '.gmdate('D, d M Y H:i:s', time() + $maxAge).' GMT', true);
header('Cache-Control: public, max-age='.$maxAge, true);
header('Pragma: ', true);
}

header('Vary: Cookie,Accept-Encoding', true);
</code>
</pre>
<p>The two most important headers with regard to caching with varnish are the following:</p>
<h3>The <code>Cache-Control</code> header.</h3>
<p>The <code>Cache-Control: public, max-age=300</code> header indicates to any clients (such as the Varnish caching proxy) that this response can be cached in public caches valid for many downstream clients. The <code>max-age</code> portion of the header indicates that the cache may store this response for 300 seconds.</p>
<p>As I understand it (possibly wrong) Varnish only looks at the <code>max-age</code> portion of the <code>Cache-Control</code> header when determining how long to store a response. Apparently it ignores the <code>Expires</code> header for its cache-expiration purposes, though this header is passed on to downstream clients.</p>
<h3>The <code>Vary</code> header</h3>
<p>The <code>Vary: Cookie,Accept-Encoding</code> header tells Varnish (and in-browser caches) that they should not respond with the cached version of a response if the request includes a cookie or a different cookie from the request that previously had its response cached. Similarly, if one client says that it accepts gzip encoding via an <code>Accept-Encoding: gzip</code> request header, then the cached response may be compressed with gzip and should not be sent in response to requests from clients that do not state that they accept gzip encoding.</p>
<p>While Varnish&rsquo;s behavior is to never cache or respond from cache when cookies are present, without the <code>Vary: Cookie</code> response header, browsers or other downstream caches may respond with a cached response valid for only anonymous users even though a cookie is now present.</p>
<p>See my notes on <a href="http://wiki.github.com/adamfranco/lazy_sessions/cache-controlling-headers">Cache-Controlling Headers</a> for more details about other headers and how they affect the Varnish cache and in-browser caches.</p>
<h2>3. Varnish (Reverse-Proxy) Configuration</h2>
<p>The <code>/etc/varnish/default.vcl</code> config file controls how Varnish responds to requests and responses, in particular whether or not it should cache or not. Below is the contents of my <code>default.vcl</code> file.</p>
<div>
<strong>Notes:</strong></p>
<ol>
<li>The backend portion is the default, you probably will want to modify this to point at your correct backend hosts and ports.</li>
<li>The <code>vcl_recv</code> and <code>vcl_hash</code> sections come directly from the <a href="https://wiki.fourkitchens.com/display/PF/Configure+Varnish+for+Pressflow?focusedCommentId=15335604">Pressflow wiki</a> and are set up to allow requests that include Google Analytics cookies to be cached while not caching requests that include other cookies.</li>
<li>The <code>vcl_fetch</code> section is the default with my addition of the lines to unset empty Set-Cookie headers that can&rsquo;t be removed from within <span class="caps">PHP</span> &lt; 5.3.</li>
</ol>
</div>
<pre>
<code>
backend default {
.host = "127.0.0.1";
.port = "80";
}

sub vcl_recv {
// Remove has_js and Google Analytics __* cookies.
set req.http.Cookie = regsuball(req.http.Cookie, "(^|;\s*)(__[a-z]+|has_js)=[^;]*", "");
// Remove a ";" prefix, if present.
set req.http.Cookie = regsub(req.http.Cookie, "^;\s*", "");
// Remove empty cookies.
if (req.http.Cookie ~ "^\s*$") {
unset req.http.Cookie;
}

// Cache all requests by default, overriding the
// standard Varnish behavior.
// if (req.request == "GET" || req.request == "HEAD") {
//   return (lookup);
// }
}

sub vcl_hash {
if (req.http.Cookie) {
set req.hash += req.http.Cookie;
}
}

sub vcl_fetch {
if (!beresp.cacheable) {
	return (pass);
}

// If using PHP &lt; 5.3 there is no way to fully delete headers, so empty
// Set-Cookie headers may be in the response. Ignore these empty headers.
if (beresp.http.Set-Cookie ~ "^\s*$") {
	unset beresp.http.Set-Cookie;
}

if (beresp.http.Set-Cookie) {
	return (pass);
}
return (deliver);
}
</code>
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2010/06/14/adding-reverse-proxy-caching-to-php-applications/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Sofa Table Complete</title>
		<link>http://www.adamfranco.com/2010/04/11/sofa-table-complete/</link>
		<comments>http://www.adamfranco.com/2010/04/11/sofa-table-complete/#comments</comments>
		<pubDate>Mon, 12 Apr 2010 00:55:09 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Life and Everything Else]]></category>
		<category><![CDATA[projects]]></category>
		<category><![CDATA[table]]></category>
		<category><![CDATA[woodworking]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=419</guid>
		<description><![CDATA[Over the course of the past year I built this cherry sofa table based on a design by Scott Gibson in Fine Woodworking&#8217;s &#34;Furniture&#34; book. All frame joinery is mortise and tenon, while the drawers use doweled rabbit joints. The finish is boiled linseed oil topped with 3 coats of Minwax wiping varnish. Building this [...]]]></description>
			<content:encoded><![CDATA[<p><object width="600" height="450"><param name="flashvars" value="offsite=true&#038;lang=en-us&#038;page_show_url=%2Fphotos%2Fadamfranco%2Fsets%2F72157623710369899%2Fshow%2F&#038;page_show_back_url=%2Fphotos%2Fadamfranco%2Fsets%2F72157623710369899%2F&#038;set_id=72157623710369899&#038;jump_to="></param><param name="movie" value="http://www.flickr.com/apps/slideshow/show.swf?v=71649"></param><param name="allowFullScreen" value="true"></param><embed type="application/x-shockwave-flash" src="http://www.flickr.com/apps/slideshow/show.swf?v=71649" allowFullScreen="true" flashvars="offsite=true&#038;lang=en-us&#038;page_show_url=%2Fphotos%2Fadamfranco%2Fsets%2F72157623710369899%2Fshow%2F&#038;page_show_back_url=%2Fphotos%2Fadamfranco%2Fsets%2F72157623710369899%2F&#038;set_id=72157623710369899&#038;jump_to=" width="600" height="450"></embed></object></p>
<p>Over the course of the past year I built this cherry sofa table based on a design by Scott Gibson in Fine Woodworking&#8217;s &quot;Furniture&quot; book.</p>
<p>All frame joinery is mortise and tenon, while the drawers use doweled rabbit joints. The finish is boiled linseed oil topped with 3 coats of Minwax wiping varnish.</p>
<p>Building this table was quite a learning experience as just about every part required techniques that I hadn&#8217;t used before. Mortise and tenon joinery, biscuits to align the table top during glue-up, doweled joints fort the drawers, quartersawn veneers for the legs, breadboard-ends, and varnish; all of these were new to me and required a bit of trial and error to get right.</p>
<p>This project certainly had its share of &quot;oops&quot; moments, but nothing that couldn&#8217;t be repaired or worked-around. I cut the bottom shelf stretcher one inch too short, but was able to cut it in half and splice in a section with a small mortise and tenon in the middle. Later, I made the hipped-tenons on which the breadboard-ends of the top sit too thin. This was repaired with the addition of some 5-minute epoxy to thicken the tenon.</p>
<p>All that remains now is to choose and install drawer-pull hardware.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2010/04/11/sofa-table-complete/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Importing users into Bugzilla</title>
		<link>http://www.adamfranco.com/2010/03/08/importing-users-into-bugzilla/</link>
		<comments>http://www.adamfranco.com/2010/03/08/importing-users-into-bugzilla/#comments</comments>
		<pubDate>Tue, 09 Mar 2010 04:11:45 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Computers and Technology]]></category>
		<category><![CDATA[Software]]></category>
		<category><![CDATA[Work/Professional]]></category>
		<category><![CDATA[Bugzilla]]></category>
		<category><![CDATA[import]]></category>
		<category><![CDATA[LDAP]]></category>
		<category><![CDATA[Perl]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=374</guid>
		<description><![CDATA[For the past 6 months our Web Application Development work-group has been Bugzilla as our issue tracker with quite a bit of success. While it has its warts, Bugzilla seems like a pretty decent issue-tracking system and is flexible enough to fit into a variety of different work-flows. One very important feature of Bugzilla is [...]]]></description>
			<content:encoded><![CDATA[<p>For the past 6 months our <a href="http://go.middlebury.edu/webservices">Web Application Development work-group</a> has been Bugzilla as our issue tracker with quite a bit of success. While it has its warts, Bugzilla seems like a pretty decent issue-tracking system and is flexible enough to fit into a variety of different work-flows. One very important feature of Bugzilla is support for LDAP authentication. This enables any Middlebury College user to log in and report a bug using their standard campus credentials.</p>
<p>While LDAP authentication works great, there is one problem: If a person has never logged into our Bugzilla, we can&#8217;t add them to the CC list of an issue. This is important for us because issues usually don&#8217;t get submitted directly to the bug tracker, but rather come in via calls, emails, tweets, and face-to-face meetings. We are then left to submit issues to Bugzilla ourselves to keep track of our to-do items. Ideally we&#8217;d add the original reporter to the bug&#8217;s CC list so that they will automatically be notified as we make progress on the issue, but their Bugzilla account must exist before we can add them to the bug.</p>
<p>Searching about the internet I wasn&#8217;t able to find anything about how to import LDAP users (or any kind of users) into Bugzilla, though I was able to find some <a href="http://groups.google.com/group/mozilla.support.bugzilla/browse_thread/thread/165d4fc1a8b4ad82/b1e31ad20bfef3f0">basic instructions</a> on how to create a single user via Bugzilla&#8217;s Perl API. To improve on the lack of user-import support I&#8217;ve created an Perl script that creates users from lines in a tab-delimited text file (<code>create_users.pl</code>) as well as a companion PHP script that will export an appropriately-formatted list of users from an Active Directory (LDAP) server (<code>export_users.php</code>).</p>
<p><span id="more-374"></span><br />
<a href='http://www.adamfranco.com/files/2010/03/BugzillaImport.zip'>BugzillaImport.zip</a> &#8212; Unzip in your Bugzilla directory, run via the command line. See below for examples.</p>
<h1>File Listings:</h1>
<h2>create_users.pl</h2>
<p>This script can safely be run repeatedly. Only new users not already in Bugzilla will be added, users matching existing email addresses will be skipped.</p>
<pre>#!/usr/bin/env perl
##########################################################
# This is a basic script to import users into Bugzilla.
#
# Users can be imported from tab-delimited text files or
# tab-delimited lines piped to STDIN. Lines should have 3
# columns: login	email	name
#
#
# Author:
#	Adam Franco (afranco@middlebury.edu)
# Date:
#	2010-03-08
# URL:
#	http://www.adamfranco.com/archives/374
# License:
#   The contents of this file are subject to the Mozilla Public
#   License Version 1.1 (the "License"); you may not use this file
#   except in compliance with the License. You may obtain a copy of
#   the License at http://www.mozilla.org/MPL/
#
#   Software distributed under the License is distributed on an "AS
#   IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
#   implied. See the License for the specific language governing
#   rights and limitations under the License.
##########################################################

use FindBin qw($Bin);
BEGIN {
    push @INC,$Bin;
    push @INC,$Bin."/lib";
    push @INC,$Bin."/lib/x86_64-linux-thread-multi";
}
use Bugzilla;
use Bugzilla::User;
use Error qw(:try);

sub usage {
    print "
Usage:
    $0 ListOfUsers1.txt [ListOfUsers2.txt [...]]
    $0 < ListOfUsers.txt

The ListOfUsers can be passed as either a file argument or passed to STDIN.

The ListOfUsers must be tab-delimited with the following columns:
login   email   name

";
    exit 1;
}

foreach (@ARGV) {
    if ($_ =~ /^-h|--help$/) {
        usage();
    }
}

my $lines = 0;
my $users = 0;
my $usersAdded = 0;
while (<>) {
    chomp; # Remove the trailing new-line.
    my($login, $email, $name) = split(/\t/, $_);

    if ($login &#038;&#038; $email &#038;&#038; $name &#038;&#038; $login =~ /[a-z0-9]+/ &#038;&#038;  $email =~ /[a-z0-9]+.*@.*[a-z0-9]+/ &#038;&#038; $name =~ /[a-z]+/) {
        if (is_available_username($email)) {
            try {
                my $user = Bugzilla::User->create({
                    login_name    => $email,
                    realname      => $name,
                    cryptpassword => '*',
                    disable_mail  => 0,
                    extern_id     => $login
                });
                print "Account for " . $user->login . " was created.\n";
                $usersAdded++;
            } catch Error with {
                my $ex = shift;
                my $error = "Error: $ex";
                $error =~ s/\n|\r/ /g;
                print $error."\n";
            };
        }

        $users++;
    }
    $lines++;
    close (ARGV) if (eof);
}

if (!$lines) {
    print "No input lines given.\n\n";
    usage();
}

print "\n$lines lines evaluated, $users user records checked, $usersAdded users added.\n";

exit 0;
</pre>
<h2>export_users.php</h2>
<pre>#!/usr/bin/env php
&lt;?php
##########################################################
# This is a basic script to export users from an
# MS Active Directory via LDAP in the format required
# by create_users.pl.
#
# Authors:
#	Adam Franco (afranco@middlebury.edu)
#	Ian McBride (imcbride@middlebury.edu)
# Date:
#	2010-03-08
# URL:
#	http://www.adamfranco.com/archives/374
# License:
#   The contents of this file are subject to the Mozilla Public
#   License Version 1.1 (the "License"); you may not use this file
#   except in compliance with the License. You may obtain a copy of
#   the License at http://www.mozilla.org/MPL/
#
#   Software distributed under the License is distributed on an "AS
#   IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
#   implied. See the License for the specific language governing
#   rights and limitations under the License.
##########################################################

$ldaphost = "ldap.example.com";
$ldapport = 389;
$ldapuser = "username";
$ldappass = "password";
$baseDN = "DC=example,DC=com";

$connection = ldap_connect($ldaphost, $ldapport);

if (!$connection) die();

if (ldap_set_option($connection, LDAP_OPT_PROTOCOL_VERSION,3) === FALSE) die();

if (ldap_set_option($connection, LDAP_OPT_REFERRALS,0) === FALSE) die();

$bind = ldap_bind($connection, $ldapuser, $ldappass);

if (!$bind) die();

$filter = "(&#038;(objectClass=User)(!(objectClass=Computer)))";

$search = ldap_search($connection, $baseDN, $filter, array("samaccountname", "mail", "givenname", "sn"));

$entries = ldap_get_entries($connection, $search);

print "samaccountname\temail\tname\n";

foreach($entries as $entry) {
  if(isset($entry['samaccountname'])) {
    print iconv('UTF-8', 'UTF-8//IGNORE', $entry['samaccountname'][0]);
  }
  print "\t";

  if(isset($entry['mail'])) {
    print iconv('UTF-8', 'UTF-8//IGNORE', $entry['mail'][0]);
  }
  print "\t";

  $name = '';
  if(isset($entry['givenname'])) {
    $name .= iconv('UTF-8', 'UTF-8//IGNORE', $entry['givenname'][0]);
  }  $name .= ' ';
  if(isset($entry['sn'])) {
    $name .= iconv('UTF-8', 'UTF-8//IGNORE', $entry['sn'][0]);
  }
  print trim($name);

  print "\n";
}
</pre>
<h1>Example Usage</h1>
<p>After unzipping the scripts in your Bugzilla directory you can use the <code>create_users.pl</code> script right away. To use <code>export_users.php</code> you will need to edit it and add your LDAP server configuration.<br />
<code>[root@hostname /var/www/htdocs/bugzilla/]# ./export_users.php | ./create_users.pl</code></p>
<p>If you&#8217;d rather import users from another source, simply create one or more tab-delimited text files that have the following columns:<br />
login&nbsp;&nbsp;&nbsp;&nbsp;email&nbsp;&nbsp;&nbsp;&nbsp;name<br />
<code>[root@hostname /var/www/htdocs/bugzilla/]# ./create_users.pl users.txt otherusers.txt</code></p>
<p>You can pipe tab-delimited data to the script as well:<br />
<code>[root@hostname /var/www/htdocs/bugzilla/]# head -n 20 users.txt | ./create_users.pl</code></p>
<h2>Update:</h2>
<ul>
<li>Changed the license statement to the MPL be compatible with the rest of Bugzilla</li>
<li>Changed the password to &#8216;*&#8217; based on Max&#8217;s suggestion</li>
</ul>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2010/03/08/importing-users-into-bugzilla/feed/</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
	</channel>
</rss>

