<?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>bdunagan &#187; development</title>
	<atom:link href="http://www.bdunagan.com/category/development/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.bdunagan.com</link>
	<description>fill the void</description>
	<lastBuildDate>Thu, 15 Dec 2011 14:04:51 +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>Creating UITableView badges like iOS Mail</title>
		<link>http://www.bdunagan.com/2011/12/14/creating-uitableview-badges-like-ios-mail/</link>
		<comments>http://www.bdunagan.com/2011/12/14/creating-uitableview-badges-like-ios-mail/#comments</comments>
		<pubDate>Wed, 14 Dec 2011 23:23:00 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=1966</guid>
		<description><![CDATA[Apple&#8217;s iOS Mail has a universal inbox on iOS 4. Very handy in general, but its unread badges complete the feature. I frequently glance down the list of email addresses I have and know exactly what the state of my inboxes are. I never have to wonder how many new emails I have. I just [...]]]></description>
			<content:encoded><![CDATA[<p><img style="float:left;padding-right:8px;" src="http://bdunagan.com/files/ios.mail.badges.outline.png"/></p>
<p>Apple&#8217;s iOS Mail has a universal inbox on iOS 4. Very handy in general, but its unread badges complete the feature. I frequently glance down the list of email addresses I have and know exactly what the state of my inboxes are. I never have to wonder how many new emails I have. I just check the numbers.</p>
<p>Recently, I needed to duplicate that feature for work. I found two existing solutions:</p>
<ul>
<li><a href="http://www.tuaw.com/2010/01/07/iphone-devsugar-simple-table-badges/">TDBadgedCell</a> (<a href="https://github.com/tmdvs/TDBadgedCell">GitHub repo</a>)</li>
<li><a href="http://digdog.tumblr.com/post/624498564/ddbadgeviewcell">DDBadgeViewCell</a> (<a href="https://github.com/digdog/DDBadgeViewCell">GitHub repo</a>)</li>
</ul>
<p>I ended up forking DDBadgeViewCell. It was concise, and I only needed to make a couple changes to it. Specifically, I wanted the badges to look exactly like Mail&#8217;s badges, in terms of alignment, colors, and shadows. I&#8217;ll go through my changes here, but you can check out <a href="https://github.com/bdunagan/DDBadgeViewCell">my GitHub fork</a> as well. See the image for the resulting look:</p>
<p><img src="http://bdunagan.com/files/DDBadgeViewCell.iphone.png"/></p>
<h3>Badge color</h3>
<p>I wanted to duplicate Mail&#8217;s look and feel, so I needed the exact colors that Mail uses for its badges. xScope made it easy.</p>
<pre class="brush: objc; title: ; notranslate">
UIColor *badgeColor = [UIColor colorWithRed:0.55 green:0.6 blue:0.69 alpha:1.];
UIColor *badgeShadowColor = [UIColor colorWithRed:0.45 green:0.49 blue:0.57 alpha:1.];
</pre>
<h3>Badge shadow</h3>
<p>If you look closely at Mail&#8217;s badges, you&#8217;ll notice the top part of the arc looks indented. The shadow gives the label a feeling of depth; the badge looks almost letter-pressed. To duplicate that look, I simply drew a darker oval one pixel higher than the original before drawing the real one.</p>
<pre class="brush: objc; title: ; notranslate">
// Draw the badge shadow.
CGContextSaveGState(context);
CGContextSetFillColorWithColor(context, currentBadgeShadowColor.CGColor);
CGMutablePathRef shadowPath = CGPathCreateMutable();
CGPathAddArc(shadowPath, NULL, badgeViewFrame.origin.x + badgeViewFrame.size.width - badgeViewFrame.size.height / 2, badgeViewFrame.origin.y + badgeViewFrame.size.height / 2, badgeViewFrame.size.height / 2, M_PI / 2, M_PI * 3 / 2, YES);
CGPathAddArc(shadowPath, NULL, badgeViewFrame.origin.x + badgeViewFrame.size.height / 2, badgeViewFrame.origin.y + badgeViewFrame.size.height / 2, badgeViewFrame.size.height / 2, M_PI * 3 / 2, M_PI / 2, YES);
CGContextAddPath(context, shadowPath);
CGContextDrawPath(context, kCGPathFill);
CFRelease(shadowPath);
CGContextRestoreGState(context);
</pre>
<h3>Pixel Perfect</h3>
<p>DDBadgeViewCell does an excellent job when there are three or more characters in the string. But as I wanted to display numbers, I needed to special-case the code a bit to get the pixels correctly aligned.</p>
<pre class="brush: objc; title: ; notranslate">
// Set up the badge's frame.
CGSize badgeTextSize = [self.cell.badgeText sizeWithFont:[UIFont boldSystemFontOfSize:18.]];
CGRect badgeViewFrame = CGRectIntegral(CGRectMake(rect.size.width - badgeTextSize.width - 24, (rect.size.height - badgeTextSize.height - 4) / 2, badgeTextSize.width + 14, badgeTextSize.height + 4));
if ([self.cell.badgeText length] &lt; 3) {
    // Fix the width for 1-2 characters.
    badgeViewFrame = CGRectIntegral(CGRectMake(rect.size.width - 46, 13, 34, 25));
}

// Draw the badge shadow oval and the badge oval...

// Draw the number on the badge.
CGContextSaveGState(context);
CGContextSetBlendMode(context, kCGBlendModeClear);
if ([self.cell.badgeText length] == 1) {
	// CGRectInset cuts off the label by a couple pixels, so need a bit more tweaking.
	CGRect badgeTextFrame = CGRectInset(badgeViewFrame, 11, 2);
	badgeTextFrame = CGRectMake(badgeTextFrame.origin.x + 1, badgeTextFrame.origin.y, badgeTextFrame.size.width + 2, badgeTextFrame.size.height);
	[self.cell.badgeText drawInRect:badgeTextFrame withFont:[UIFont boldSystemFontOfSize:18]];
}
else if ([self.cell.badgeText length] == 2) {
	// CGRectInset cuts off the label by a couple pixels, so need a bit more tweaking.
	CGRect badgeTextFrame = CGRectInset(badgeViewFrame, 6, 2);
	badgeTextFrame = CGRectMake(badgeTextFrame.origin.x + 1, badgeTextFrame.origin.y, badgeTextFrame.size.width + 4, badgeTextFrame.size.height);
	[self.cell.badgeText drawInRect:badgeTextFrame withFont:[UIFont boldSystemFontOfSize:18]];
}
else {
	CGRect badgeTextFrame = CGRectInset(badgeViewFrame, 7, 2);
	badgeTextFrame = CGRectMake(badgeTextFrame.origin.x, badgeTextFrame.origin.y + 1, badgeTextFrame.size.width + 4, badgeTextFrame.size.height);
    [self.cell.badgeText drawInRect:badgeTextFrame withFont:[UIFont boldSystemFontOfSize:18.]];
    CGContextRestoreGState(context);
}
CGContextRestoreGState(context);
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2011/12/14/creating-uitableview-badges-like-ios-mail/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Modal views in universal iOS apps</title>
		<link>http://www.bdunagan.com/2011/12/14/modal-views-in-universal-ios-apps/</link>
		<comments>http://www.bdunagan.com/2011/12/14/modal-views-in-universal-ios-apps/#comments</comments>
		<pubDate>Wed, 14 Dec 2011 15:08:22 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=1953</guid>
		<description><![CDATA[On the iPhone, it&#8217;s quite normal to have an app slide a sheet up to create something or change settings. But on the iPad, that same view should be displayed in a popover. In a universal app, the key is shared code and XIBs for both approaches. The code below is all that I needed [...]]]></description>
			<content:encoded><![CDATA[<p><img style="float:left; padding-right:0px;" src="http://bdunagan.com/files/universal.link.png"/></p>
<p>On the iPhone, it&#8217;s quite normal to have an app slide a sheet up to create something or change settings. But on the iPad, that same view <a href="http://developer.apple.com/library/ios/#featuredarticles/ViewControllerPGforiPhoneOS/iPadControllers/iPadControllers.html">should be displayed in a popover</a>. In a universal app, the key is shared code and XIBs for both approaches.</p>
<p>The code below is all that I needed to have a Settings sheet appear correctly in both an iPhone and iPad app. I simply identify the device type and then either present a popover controller or a modal view controller, using the same XIB. Clean code. Consistent interface.</p>
<pre class="brush: objc; title: ; notranslate">
// Main View Controller
- (IBAction)showSheet:(id)sender {
	if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
		// iPad
		if (!self.popoverController.popoverVisible) {
			self.popoverController = [[[UIPopoverController alloc] initWithContentViewController:self.myNavigationController] autorelease];
			self.popoverController.popoverContentSize = CGSizeMake(400, 400);
			self.popoverController.contentViewController.contentSizeForViewInPopover = CGSizeMake(400, 400);
			self.popoverController.delegate = self;
			// Present the popover from the bar button.
			[self.popoverController presentPopoverFromBarButtonItem:self.navigationItem.rightBarButtonItem permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
		}
	}
	else {
		// iPhone
		[self.navigationController presentModalViewController:self.myNavigationController animated:YES];
	}
}

- (void)popoverControllerDidDismissPopover:(UIPopoverController *)aPopoverController {
	[self.popoverController dismissPopoverAnimated:YES];
}

- (MyNavigationController *)myNavigationController {
	if (myNavigationController == nil) {
		self.myNavigationController = [[UINavigationController alloc] initWithRootViewController:self.mySheetController] autorelease];
	}
	return myNavigationController;
}

- (MySheetController *)mySheetController {
	if (mySheetController == nil) {
		self.mySheetController = [[MySheetController alloc] initWithNibName:@&quot;MySheetView&quot; bundle:nil] autorelease];
	}
	return mySheetController;
}

// Sheet View Controller
- (void)viewWillAppear:(BOOL)animated {
	self.contentSizeForViewInPopover = CGMakeSize(400, 400);
	[super viewWillAppear:animated];
}
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2011/12/14/modal-views-in-universal-ios-apps/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Ad-Hoc and App Store IPAs with xcrun</title>
		<link>http://www.bdunagan.com/2011/12/12/ad-hoc-and-app-store-ipas-with-xcrun/</link>
		<comments>http://www.bdunagan.com/2011/12/12/ad-hoc-and-app-store-ipas-with-xcrun/#comments</comments>
		<pubDate>Mon, 12 Dec 2011 01:14:19 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=1932</guid>
		<description><![CDATA[Xcode 4&#8242;s &#8220;Build and Archive&#8221; feature is extremely convenient. With it, I can build a distribution version of an app and archive it away, so that later, in Xcode Organizer, I can click on that instance and submit it to the App Store. Brilliant for individual developers. But at my work, we wanted a bit [...]]]></description>
			<content:encoded><![CDATA[<p><img style="float:left; padding-right:20px;" src="http://bdunagan.com/files/xcode.icon.png"/></p>
<p>Xcode 4&#8242;s &#8220;Build and Archive&#8221; feature is extremely convenient. With it, I can build a distribution version of an app and archive it away, so that later, in Xcode Organizer, I can click on that instance and submit it to the App Store. Brilliant for individual developers. But at my work, we wanted a bit more: automation and multiple IPAs from a single binary. Both are easy with <tt>xcrun</tt>, part of Apple&#8217;s Developer Tools. (Thanks to <a href="http://stackoverflow.com/questions/2664885/xcode-build-and-archive-from-command-line">Stackoverflow for the pointer</a>!)</p>
<h3>Standardized Keys and Certificates</h3>
<p>At work, we wanted a standard set of keys and certificates to use for official builds. I went through the standard Apple Developer Center process of creating a certificate signing request with Keychain Access to generate the local keypair, and I created a distribution certificate with that signing request and loaded that into Keychain Access. Then in Keychain Access, I exported the certificate as <tt>distribution.cer</tt> and the private key as <tt>distribution.p12</tt>. Xcode needs these two files to correctly use the provisioning profile.</p>
<p>One small hiccup was &#8220;login&#8221; versus &#8220;System&#8221; in Keychain Access. I easily imported those two exported files into the &#8220;login&#8221; keychain in Keychain Access. However, Keychain Access would not import the private key into the &#8220;System&#8221; keychain and gave the following error: &#8220;An error has occurred. Unable to import an item.&#8221; I needed to use the &#8220;System&#8221; keychain, as our build system runs with elevated privileges. To get around this error, I simply needed to import the certificate and private key via Terminal instead:</p>
<pre class="brush: plain; title: ; notranslate">
# Run in Terminal.
sudo security import /path/to/distribution.cer -k /Library/Keychains/System.keychain
sudo security import /path/to/distribution.p12 -k /Library/Keychains/System.keychain
</pre>
<p>Don&#8217;t forget to change the &#8220;Access Control&#8221; for the private key to &#8220;Allow any application to access this item&#8221; for &#8220;System.keychain&#8221;. Otherwise, you&#8217;ll get a build error about &#8220;user interaction is not allowed&#8221;.</p>
<p>At this point, I had a copy of the certificate and private key in &#8220;login&#8221; and &#8220;System&#8221; in Keychain Access. I simply needed two provisioning profiles based on that certificate: an ad-hoc profile and an app store profile. I generated both in the iOS Provisioning Profile, <tt>Adhoc.mobileprovision</tt> and <tt>Appstore.mobileprovision</tt>, and imported those into Xcode.</p>
<p>With the certificate, private key, and two provisioning profiles loaded up, automating the build was straight-forward. In fact, by selecting the correct default configuration (Xcode 3.2.6), I can simply run <tt>Xcodebuild</tt> with no arguments to generate an App Store distribution app bundle. </p>
<h3>Multiple IPAs from a single binary</h3>
<p>Automated builds are nothing new or novel. There are some great suggestions for how to <a href="http://www.benzado.com/blog/iphonedev-good-practices">structure your Xcode configurations</a> to simplify automation. What I really wanted were two files at the end: <tt>App.AdHoc.ipa</tt> and <tt>App.AppStore.ipa</tt>, ready to be tested ad-hoc or submitted to the App Store. Enter <tt>xcrun</tt>.</p>
<p>Apple distributes <tt>xcrun</tt> as part of its Xcode Developer package. The command line utility will take an app bundle and convert it into an IPA file with the supplied provisioning profile. The key part is the supplied provisioning profile can be different from the original one. Here is how I call it:</p>
<pre class="brush: plain; title: ; notranslate">
xcrun -sdk iphoneos PackageApplication
   /path/to/bundle/AppName.app
   -o /path/to/bundle/AppName.ipa
   --sign &quot;iPhone Distribution&quot;
   --embed /path/to/certificate
</pre>
<p>With this command, I can run a build and then generate two IPA files: one for ad-hoc distribution or one for App Store submission. All with the same binary. The community and the tools have progressed a bit since <a href="http://furbo.org/2008/11/12/the-final-test/">furbo&#8217;s &#8220;The final test&#8221; in 2008</a>.</p>
<h3>Over-The-Air ad-hoc distribution</h3>
<p>Apple added support for over-the-air (OTA) distribution in iOS 4. It&#8217;s the best way to distribute builds to QA, much better than syncing through iTunes. <a href="http://nachbaur.com/blog/building-ios-apps-for-over-the-air-adhoc-distribution">These instructions</a> worked perfectly for me. You don&#8217;t need an Enterprise iOS account to do this; I think that simply lets you <a href="http://stackoverflow.com/questions/5546581/how-to-distribute-ios-application-wirelessly-without-managing-udids-and-recompil">get around managing UDIDs</a>. Seriously, it&#8217;s easy and awesome.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2011/12/12/ad-hoc-and-app-store-ipas-with-xcrun/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Deleting large AWS S3 buckets</title>
		<link>http://www.bdunagan.com/2011/11/21/deleting-large-aws-s3-buckets/</link>
		<comments>http://www.bdunagan.com/2011/11/21/deleting-large-aws-s3-buckets/#comments</comments>
		<pubDate>Mon, 21 Nov 2011 18:24:07 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=1918</guid>
		<description><![CDATA[2011.12.08 Update: Amazon now supports multi-object delete in a single API request, up to 1k objects. Amazon&#8217;s AWS S3 is awesome. However, there are times when the API makes certain tasks a bit hard. Today, I wanted to delete a bucket with 100k files. AWS does not support deleting a bucket with files inside; you [...]]]></description>
			<content:encoded><![CDATA[<p><b>2011.12.08 Update</b>: Amazon now supports <a href="http://docs.amazonwebservices.com/AmazonS3/latest/API/index.html?multiobjectdeleteapi.html">multi-object delete</a> in a single API request, up to 1k objects.</p>
<p><img style="float:left;padding-right:10px;" src="http://bdunagan.com/files/aws.png"/></p>
<p>Amazon&#8217;s AWS S3 is awesome. However, there are times when the API makes certain tasks a bit hard. Today, I wanted to delete a bucket with 100k files. AWS does not support deleting a bucket with files inside; you have to delete the files first.</p>
<p>First, I tried <a href="http://amazon.rubyforge.org/"><tt>aws/s3</tt></a>. It supports <tt>:force</tt> when deleting buckets, in case they have files in them. That call was taking a long time, so I poked around the code. Unfortunately, the gem didn&#8217;t make some magic API call. It just looped:</p>
<pre class="brush: ruby; title: ; notranslate">
def delete_all
  each do |object|
    object.delete
  end
  self
end
</pre>
<p>Next, I tried to find out how geemus&#8217;s brilliant <tt>fog</tt> gem handles this need. <a href="http://groups.google.com/group/ruby-fog/browse_thread/thread/3e12a89ae899f5f2">It doesn&#8217;t</a>, because providers don&#8217;t.</p>
<p>Finally, I gave up that one API call dream and looked for what other people do. They use pagination and threads, and I ended up forking <a href="https://github.com/SFEley/s3nuke">SFEley&#8217;s <tt>s3nuke</tt> script</a> from GitHub. In the process of porting his solution from RightAWS to Fog, I discovered that fog handles auto-pagination internally. No need to mess with <tt>is_truncated</tt> or <tt>:marker</tt>. Quite nice.</p>
<p>This Ruby script deleted an S3 bucket with 100k files in 15 minutes.</p>
<p><script src="https://gist.github.com/1383301.js?file=s3-delete-bucket.rb"></script></p>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2011/11/21/deleting-large-aws-s3-buckets/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>iPhone Tip: Tether to your Mac</title>
		<link>http://www.bdunagan.com/2011/11/17/iphone-tip-tether-to-your-mac/</link>
		<comments>http://www.bdunagan.com/2011/11/17/iphone-tip-tether-to-your-mac/#comments</comments>
		<pubDate>Thu, 17 Nov 2011 15:25:08 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=1912</guid>
		<description><![CDATA[No, not the iOS Personal Hotspot. I meant the other direction. Here is how to share your Mac&#8217;s internet connection with your iPhone or iPad. Simply go into System Preferences and click on &#8220;Sharing&#8221;. In &#8220;Sharing&#8221;, select &#8220;Internet Sharing&#8221; and click &#8220;Wi-Fi Options&#8230;&#8221; to configure the network name. Then, just check the box for &#8220;Internet [...]]]></description>
			<content:encoded><![CDATA[<p>No, not the <a href="http://support.apple.com/kb/HT4517">iOS Personal Hotspot</a>. I meant the other direction. Here is how to share your Mac&#8217;s internet connection with your iPhone or iPad.</p>
<p>Simply go into System Preferences and click on &#8220;Sharing&#8221;. In &#8220;Sharing&#8221;, select &#8220;Internet Sharing&#8221; and click &#8220;Wi-Fi Options&#8230;&#8221; to configure the network name. Then, just check the box for &#8220;Internet Sharing&#8221;.</p>
<div style="padding:10px">
<a class="single_image" href="http://bdunagan.com/files/share_mac_internet_network.png"><img src="http://bdunagan.com/files/share_mac_internet_network.small.png"/></a>
</div>
<div style="padding:10px">
<a class="single_image" href="http://bdunagan.com/files/share_mac_internet_settings.png"><img src="http://bdunagan.com/files/share_mac_internet_settings.small.png"/></a>
</div>
<p>Very useful in some instances.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2011/11/17/iphone-tip-tether-to-your-mac/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Git Tip: Better &#8216;git log&#8217;</title>
		<link>http://www.bdunagan.com/2011/11/13/git-tip-better-git-log/</link>
		<comments>http://www.bdunagan.com/2011/11/13/git-tip-better-git-log/#comments</comments>
		<pubDate>Sun, 13 Nov 2011 19:14:56 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=1897</guid>
		<description><![CDATA[A while ago, I followed this blog&#8217;s advice and added a little color to &#8216;git log&#8217;. This original &#8216;git log&#8217; command displays useful tidbits like 7c UUID, relative date, description, tag, and user. But today, I wanted a bit more: an short absolute date and lines changed. Seeing the number of lines that changed per [...]]]></description>
			<content:encoded><![CDATA[<p>A while ago, I followed <a href="http://www.jukie.net/bart/blog/pimping-out-git-log">this blog&#8217;s advice</a> and added a little color to &#8216;git log&#8217;. This original &#8216;git log&#8217; command displays useful tidbits like 7c UUID, relative date, description, tag, and user.</p>
<p>But today, I wanted a bit more: an short absolute date and lines changed. Seeing the number of lines that changed per commit gives a better idea about the magnitude of the changes. Changing the date format was easy; adding the number of lines that changed was harder.</p>
<p>Below is a gist for the Ruby script I wrote. Instructions on setting it up as a Bash alias are embedded in it.</p>
<p><script src="https://gist.github.com/1362507.js?file=git.log.with.lines.changed.rb"></script></p>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2011/11/13/git-tip-better-git-log/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Migrating JIRA to Bugzilla</title>
		<link>http://www.bdunagan.com/2011/11/07/migrating-jira-to-bugzilla/</link>
		<comments>http://www.bdunagan.com/2011/11/07/migrating-jira-to-bugzilla/#comments</comments>
		<pubDate>Mon, 07 Nov 2011 21:36:05 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=1882</guid>
		<description><![CDATA[Recently, my team needed to migrate from Atlassian&#8217;s JIRA to Mozilla&#8217;s Bugzilla. I expected this process to be uncommon but not unheard of. Not so. Atlassian has a nice tool for migrating Bugzilla data into its JIRA system, but the one googleable conversation I could find about migrating JIRA to Bugzilla was in 2006. I&#8217;ll [...]]]></description>
			<content:encoded><![CDATA[<p>Recently, my team needed to migrate from Atlassian&#8217;s JIRA to Mozilla&#8217;s Bugzilla. I expected this process to be uncommon but not unheard of. Not so. Atlassian has a nice tool for migrating Bugzilla data into its JIRA system, but <a href="http://fixunix.com/mozilla/410165-migrate-jira-bugzilla.html">the <i>one</i> googleable conversation</a> I could find about migrating JIRA to Bugzilla was in 2006. </p>
<p>I&#8217;ll describe the process I used to migrate around 6K bugs.</p>
<h3>Tools</h3>
<p><i>Bugzilla&#8217;s importxml.pl</i>: Bugzilla does ship with an <a href="http://www.bugzilla.org/docs/3.0/html/api/importxml.html">import script</a>. However, it&#8217;s specifically designed for moving bugs from one Bugzilla instance to another. As input, it takes an XML file of one or more bugs, assuming you just used the XML export built into Bugzilla. Makes sense. It meant that I just extract all the issues from JIRA and reformat them into Bugzilla&#8217;s <a href="http://bzr.mozilla.org/bugzilla/3.6/annotate/head:/bugzilla.dtd">DTD</a>. I found it very helpful to add the <tt>--verbose</tt> flag for better errors.</p>
<pre class="brush: plain; title: ; notranslate">
./importxml.pl --verbose bug.xml
# The log below represents a successful import.
OK: Bug http://example.com/bugzilla/show_bug.cgi?id=1 imported as bug 1.

http://example.com/bugzilla/show_bug.cgi?id=1
</pre>
<p><i>Bugzilla&#8217;s checksetup.pl</i>: Bugzilla will check your setup with this script and let you know if you are missing any Perl modules. In particular, for importing, you will need <tt>XML::Twig</tt>. Oddly, when I started the import process, I found that I was still missing <tt>XML::Parser</tt>, so I had to install that from CPAN as well. Also, the script changed the ownership and permissions of the bugzilla directory such that it was no longer accessible on the web; I had to recursively revert those like so:</p>
<pre class="brush: plain; title: ; notranslate">
chmod -R 754 bugzilla
chown -R nobody:apache bugzilla
</pre>
<p><i>nokogiri</i>: This handy Ruby gem allowed me to use XPath to search through the JIRA XML files and extract specific fields and values. It&#8217;s extremely useful. It installs effortlessly on Lion (10.7) with a simple <tt>gem install nokogiri</tt>. Unfortunately, I was using Snow Leopard (10.6). Installing the gem on that OS was a small battle in itself. Finally, I used <a href="https://gist.github.com/746966">this gist</a> and then <a href="https://github.com/tenderlove/nokogiri/issues/381">followed steps here</a>. That worked for me. Here are some handy snippets of Nokogiri in action:</p>
<pre class="brush: ruby; title: ; notranslate">
# Element contains
doc.xpath(&quot;//h1[contains(.,'Could not find issue with issue key')]&quot;)
# Element value
doc.xpath(&quot;//item/updated&quot;)[0].text
# Attribute value
doc.xpath(&quot;//item/reporter/@username&quot;).text
# Value of subelement for element
doc.xpath(&quot;//attachment&quot;).each do |attachment|
  attachment_id = attachment.attributes[&quot;id&quot;].value
end
</pre>
<p><i>JIRA Issues</i>: There are a couple ways to export issues from JIRA. One obvious way is to simply search for the issues you want and click &#8220;View&#8221; at the top to select a different format, like XML. While straight-forward, this method has two significant downsides. First, data loss: comments are not included. Second, size: thousands of issues take a long time to process as a single XML file, and neither JIRA nor Chrome seemed happy about the size.</p>
<p>An alternative to this export is using <tt>curl</tt> to export issues individually. This process includes comments and involves lots of quick JIRA queries. As an added bonus, I can avoid any XML SAX state logic that a single large XML file would have needed, so I can focus on transforming issues into bugs in isolation. Sweet.</p>
<pre class="brush: ruby; title: ; notranslate">
# Extract JIRA issues. (Figure out how many issues to include.)
(1..1000).each do |i|
  # Use curl with credentials to extract each JIRA issue's XML.
  xml = %x[curl -u username:password https://jira.example.com/si/jira.issueviews:issue-xml/BUG-#{i}/BUG-#{i}.xml]
  # Save the XML to a file.
  f=File.new(&quot;BUG-#{i}.xml&quot;,&quot;w&quot;)
  f.write(xml)
  f.close
end
</pre>
<p><i>JIRA Attachments</i>: In addition to the issues, I wanted to extract all of the attachments. Luckily, JIRA provides a standard HTTP API for getting these files. We just need all of the attachment IDs from each extracted JIRA issue to access its attachments. We&#8217;ll save those to an <tt>attachments</tt> directory. Bugzilla actually imports attachments as embedded base64 strings in the XML files, but we&#8217;ll address that later. For now, we just want to save the attachments out of JIRA.</p>
<pre class="brush: ruby; title: ; notranslate">
# Download all the JIRA attachments. We'll need to convert them to base64 and provide them inline to Bugzilla.
Dir.mkdir(&quot;attachments&quot;) if !Dir.exists?(&quot;attachments&quot;)
(1..1000).each do |i|
  puts i
  # Open the JIRA issue.
  doc = Nokogiri::XML(open(File.expand_path(&quot;BUG-#{i}.xml&quot;)))
  # Find all the attachment references.
  doc.xpath(&quot;//attachment&quot;).each do |attachment|
    # Get the attachment ID and file name.
    attachment_id = attachment.attributes[&quot;id&quot;].value
    file_name = attachment.attributes[&quot;name&quot;].value
    # Get the attachment contents using curl.
    contents = %x[curl -u username:password https://jira.example.com/secure/attachment/#{attachment_id}/#{file_name}]
    # Save the attachment.
    Dir.mkdir(&quot;attachments/BUG-#{i}&quot;) if !Dir.exists?(&quot;attachments/BUG-#{i}&quot;)
    f=File.new(&quot;attachments/BUG-#{i}/#{attachment_id}_#{file_name}&quot;,&quot;w&quot;)
    f.write(contents)
    f.close
  end
end
</pre>
<p><i>Bugzilla Field Values</i>: Bugzilla will not automatically create people, products, versions, components, or milestones during the import process. Those need to already exist. Otherwise, Bugzilla will use the default product and component. In my case, they did already exist, but the names had been changed. To handle these changes, I map JIRA strings to Bugzilla strings in my main script.</p>
<h3>jira2bugzilla.rb</h3>
<p>At this point, we have all the tools and the data we need to tranform JIRA issues into Bugzilla bugs. I committed the full <tt>jira2bugzilla.rb</tt> Ruby script to a <a href="https://github.com/bdunagan/jira2bugzilla">GitHub repo</a>. It doesn&#8217;t work out of the box, as there are quite a few instance-specific variables, but the script provides a nice base. I&#8217;ll touch on a couple points:</p>
<p><i>Attachments</i>: Bugzilla expects attachments embedded in the XML file, so we need to convert our binary files into base64 strings and then include them inline. Below is Ruby code to convert to base64. Keep in mind that the import process takes far longer when attachments are included.</p>
<pre class="brush: ruby; title: ; notranslate">
# Get the attachment ID and name.
attachment_id = attachment.attributes[&quot;id&quot;].value
file_name = attachment.attributes[&quot;name&quot;].value
# Figure out where we saved it.
attachment_path = &quot;BUG/#{bug_id}/#{attachment_id}_#{file_name}&quot;
# Encode the file as base64.
encoded_file = Base64.encode64(IO.read(&quot;#{attachment_path}&quot;))
</pre>
<p><i>Description</i>: While JIRA gives the bug description its own element (<tt>description</tt>), Bugzilla considers it the first comment on the bug. When rewriting the JIRA issues into a Bugzilla bug, I needed to migrate the description into the first comment.</p>
<p><i>Severity</i>: JIRA doesn&#8217;t seem to have a notion of severity, so I assigned a default severity to all the bugs.</p>
<p><i>QA Contact</i>: JIRA doesn&#8217;t seem to have a notion of QA contact, so when setting up the components, be sure to assign the default QA contact correctly. The import script will assign each bug to the correct person.</p>
<p>Again, see <a href="https://github.com/bdunagan/jira2bugzilla">this GitHub repo</a> for the full <tt>jira2bugzilla.rb</tt> script. I simply run <tt>ruby jira2bugzilla.rb</tt> in the directory with all of the JIRA XML issues.</p>
<h3>Waiting for Import</h3>
<p>At this point, we have converted all the JIRA issues into Bugzilla bugs. Next, we transfer them onto the Bugzilla server into a subdirectory of the bugzilla installation, like <tt>bugzilla/bugs/</tt>. To import the bugs, I ran the following one-liner. It finds all the XML files, sorts them alphabetically, and feeds each one by one into the Bugzilla import script.</p>
<pre class="brush: plain; title: ; notranslate">
find . -name &quot;BUG-*.xml&quot; | sort -n | xargs -n 1 ./importxml.pl --verbose &amp;&gt; bug-import.log
</pre>
<p>The script imported 6K bugs in two hours. However, I didn&#8217;t include attachments. When I tried to import a single bug with a 5MB attachment, it took around thirty minutes. With multiple gigabytes of attachments, I opted to not include them. </p>
<p>Most importantly, check the log. The above one-liner pipes all of the log information to <tt>bug-import.log</tt>. Run <tt>tail -f bug-import.log</tt> while it&#8217;s running to make sure the process is working. Grep through the output afterwards for terms like &#8220;Bad&#8221; or &#8220;Error&#8221; to ensure all the bugs were imported. With 6K bugs, most imported correctly, but a few did not.</p>
<h3>Atlassian</h3>
<p>I just wanted to highlight that migrating away from Atlassian&#8217;s JIRA is not a reflection of their product. I was simply tasked with making it happen. Frankly, Atlassian has been doing an amazing job at steadily growing into a vertically integrated company. They received $60M in VC funding in 2010 to grow their business, and they&#8217;ve scooped up businesses like BitBucket and SourceTree. Just a couple weeks ago, they announced that their main products will now be available as cloud services. Atlassian seems to have a long-term strategy that they&#8217;re executing very well.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2011/11/07/migrating-jira-to-bugzilla/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>EC2 SSH on the iPad with Panic Prompt</title>
		<link>http://www.bdunagan.com/2011/10/30/ec2-ssh-on-the-ipad-with-panic-prompt/</link>
		<comments>http://www.bdunagan.com/2011/10/30/ec2-ssh-on-the-ipad-with-panic-prompt/#comments</comments>
		<pubDate>Sun, 30 Oct 2011 15:59:56 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=1833</guid>
		<description><![CDATA[Back in April, Panic shipped a beautiful SSH app for iPhone and iPad: Prompt. Quite a few comments on that announcement blog post concerned EC2. Many people wanted to SSH into their Amazon cloud, but they couldn&#8217;t figure out the keypair situation. A day later, Joe Cheng covered steps that worked for him. Unfortunately, his [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://itunes.apple.com/us/app/prompt/id421507115?mt=8"><img style="float:left; padding-right:12px;" src="http://bdunagan.com/files/panic.prompt.png"/></a></p>
<p>Back in April, Panic <a href="http://www.panic.com/blog/2011/04/introducing-prompt-ssh-for-ios/">shipped</a> a beautiful SSH app for iPhone and iPad: <a href="http://itunes.apple.com/us/app/prompt/id421507115?mt=8">Prompt</a>. Quite a few comments on that announcement blog post concerned EC2. Many people wanted to SSH into their Amazon cloud, but they couldn&#8217;t figure out the keypair situation. A day later, Joe Cheng covered <a href="http://jcheng.wordpress.com/2011/04/15/using-panic-prompt-with-ec2/">steps that worked for him</a>. Unfortunately, his steps didn&#8217;t work for me. Here is what I did:</p>
<ol>
<li>Buy <a href="http://itunes.apple.com/us/app/prompt/id421507115?mt=8">Prompt</a>. I&#8217;m using v1.2.2.</li>
<li>Find your keypair file. This is <i>not</i> the <tt>.pem</tt> file. This is the one you use to SSH into your instances without a password: <tt>ssh -i path/to/keypair_file name@ip_address</tt>.</li>
<li>Duplicate that keypair file. I named it <tt>aws</tt>.</li>
<li>Look at the duplicated file. Mine contained the key signature as the first line. (See below.) Remove the key signature. Prompt doesn&#8217;t like it.
<pre class="brush: plain; title: ; notranslate">
KEYPAIR	keypair-name	12:34:56:78:90:12:34:56:78:90:12:34:56:78:90:12:34:56:78:90
-----BEGIN RSA PRIVATE KEY-----
...key...
-----END RSA PRIVATE KEY-----
</pre>
</li>
<li>Sync your iOS device with iTunes, click on &#8220;Apps&#8221;, scroll down, and find &#8220;File Sharing&#8221;.</li>
<li>Click on Prompt in the list of apps, click &#8220;Add&#8230;&#8221;, select the duplicate keypair file (<tt>aws</tt>), and add it.</li>
<li>Sync your iOS device again to transfer the files over.</li>
<li>Launch Prompt. You&#8217;ll see a little key icon next to &#8220;Password&#8221;. Tap it.</li>
<li>As soon as you tap the key icon, you should see a list of keys (and not the yellow popup). My list has &#8220;None&#8221; and &#8220;aws&#8221;. Looking back at iTunes, you&#8217;ll notice the keypair file is no longer listed in &#8220;Prompt Documents&#8221;.</li>
<li>No need to worry about generating a public key with <tt>ssh-keygen</tt>.</li>
</ol>
<p><img src="http://bdunagan.com/files/panic.prompt.screenshot.png"/></p>
<p>As a bonus, I found <a href="http://blog.23x.net/659/panic-prompt-ssh-ios.html">this great tip</a> on using <tt>screen</tt> to create or reattach to a terminal session: <tt>screen -DDRS ios</tt>.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2011/10/30/ec2-ssh-on-the-ipad-with-panic-prompt/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Server Tip: Screen</title>
		<link>http://www.bdunagan.com/2011/10/29/server-tip-screen/</link>
		<comments>http://www.bdunagan.com/2011/10/29/server-tip-screen/#comments</comments>
		<pubDate>Sat, 29 Oct 2011 21:07:10 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=1819</guid>
		<description><![CDATA[Ever lose an SSH connection due to a long-running job? Start using screen. It&#8217;s an extremely handy Linux app that lets you create and manage terminal sessions. Much like you can VNC into an already running remote Mac UI and RDC into an already running remote Windows UI, screen allows you to SSH into a [...]]]></description>
			<content:encoded><![CDATA[<p>Ever lose an SSH connection due to a long-running job? Start using <tt>screen</tt>. It&#8217;s an extremely handy Linux app that lets you create and manage terminal sessions. Much like you can VNC into an already running remote Mac UI and RDC into an already running remote Windows UI, <tt>screen</tt> allows you to SSH into a server and reattach to an already running terminal session. Awesome. Here are some handy shortcuts:</p>
<p><script src="https://gist.github.com/1325096.js?file=gistfile1.sh"></script></p>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2011/10/29/server-tip-screen/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Ruby Tip: Sort a hash recursively</title>
		<link>http://www.bdunagan.com/2011/10/23/ruby-tip-sort-a-hash-recursively/</link>
		<comments>http://www.bdunagan.com/2011/10/23/ruby-tip-sort-a-hash-recursively/#comments</comments>
		<pubDate>Sun, 23 Oct 2011 10:00:00 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=1807</guid>
		<description><![CDATA[A while ago, I found a great Stackoverflow answer for sorting a hash in Ruby: But recently, I wanted to take it a bit further and recursively sort all the hashes in an object, even if they are inside of an array. Here is a snippet to do that:]]></description>
			<content:encoded><![CDATA[<p>A while ago, I found a <a href="http://stackoverflow.com/questions/983444/sorting-hash-of-hashes-by-value-and-return-the-hash-not-an-array/983575#983575">great Stackoverflow answer for sorting a hash in Ruby</a>:</p>
<pre class="brush: ruby; title: ; notranslate">
class Hash
  def sorted_hash(&amp;block)
    self.class[sort(&amp;block)]
  end
end
</pre>
<p>But recently, I wanted to take it a bit further and recursively sort <i>all</i> the hashes in an object, even if they are inside of an array. Here is a snippet to do that:</p>
<pre class="brush: ruby; title: ; notranslate">
class Hash
  def sorted_hash(&amp;block)
    self.class[
      self.each do |k,v|
        self[k] = v.sorted_hash(&amp;block) if v.class == Hash
        self[k] = v.collect {|a| a.sorted_hash(&amp;block)} if v.class == Array
      end.sort(&amp;block)]
  end
end
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2011/10/23/ruby-tip-sort-a-hash-recursively/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>

