fe7b5dcbcdcc

Merge with the MODI change.
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Sat, 01 May 2010 00:19:01 -0400
parents 37f55dfd4e5f (diff) 2b83293c583f (current diff)
children 99a89bbb955a
branches/tags (none)
files

Changes

--- a/.venv	Wed Mar 03 10:28:53 2010 -0500
+++ b/.venv	Sat May 01 00:19:01 2010 -0400
@@ -1,1 +1,1 @@
-stevelosh-hyde
+stevelosh
--- a/DESIGN.mdown	Wed Mar 03 10:28:53 2010 -0500
+++ b/DESIGN.mdown	Sat May 01 00:19:01 2010 -0400
@@ -3,4 +3,10 @@
 Images
 ======
 
-Full images for posts should be 600 pixels wide (or less).
\ No newline at end of file
+Full images for posts should be 600 pixels wide (or less).
+
+Headers
+=======
+
+Blog and project content block should use h2 and lower. h1 elements are
+reserved for the layout.
\ No newline at end of file
--- a/content/blog/2009/08/a-guide-to-branching-in-mercurial.html	Wed Mar 03 10:28:53 2010 -0500
+++ b/content/blog/2009/08/a-guide-to-branching-in-mercurial.html	Sat May 01 00:19:01 2010 -0400
@@ -7,9 +7,9 @@
     categories: ["programming"]
 %}
 
-{% block article %}
+{% block article_class %}with-diagrams{% endblock %}
 
-<div class="with-diagrams">
+{% block article %}
 
 I've been hanging out in the [#mercurial][hg-irc] and [#bitbucket][bb-irc]
 channels on freenode a lot lately, and I've noticed a topic that comes up a
@@ -402,6 +402,4 @@
 the git side of things (I don't use git any more than I have to) or have any
 questions please let me know!
 
-</div>
-
 {% endblock %}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/content/blog/2010/02/mercurial-workflows-branch-as-needed.html	Sat May 01 00:19:01 2010 -0400
@@ -0,0 +1,149 @@
+{% extends "_post.html" %}
+
+{% hyde
+    title: "Mercurial Workflows: Branch As Needed"
+    snip: "Part 1 of several."
+    created: 2010-02-28 14:00:00
+    categories: ["programming"]
+%}
+
+{% block article_class %}with-diagrams{% endblock %}
+
+{% block article %}
+
+A while ago [Vincent Driessen][] posted an example of [a successful git
+branching model][gitbranch]. A lot of git users found that article very
+helpful, and [Dirkjan Ochtman][] [told me][djctweet] I should write a similar
+article for Mercurial users.
+
+[Vincent Driessen]: http://nvie.com/
+[gitbranch]: http://nvie.com/git-model
+[Dirkjan Ochtman]: http://dirkjan.ochtman.nl/
+[djctweet]: http://twitter.com/djco/status/8061889499
+
+I decided that I didn't want to just write a single entry about one branching
+workflow. Mercurial is flexible enough to support many different workflows and
+some of them will fit a given project better than others. Instead I'm going to
+write a series of posts, each one about a particular workflow.
+
+I'm going to start off with the simplest example I can think of: "Don't worry
+about branching at all, just deal with it whenever it happens."
+
+**Note:** In this series I'm going to assume you're comfortable with basic
+Mercurial commands and you know how Mercurial's various forms of branching
+work. If you need some review on Mercurial's commands you should look at the
+[hg book][]. If you need more information on branching concepts you might like
+my [Guide to Branching in Mercurial][hgbranching].
+
+[hg book]: http://hgbook.red-bean.com/
+[hgbranching]: /blog/2009/08/a-guide-to-branching-in-mercurial/
+
+[TOC]
+
+"Branch as Needed" In a Nutshell
+--------------------------------
+
+The general idea of this workflow is that you don't worry about branching
+until it actually happens.
+
+The benefit is that it takes no extra work up front and keeps things very
+simple.
+
+The drawback is that it doesn't scale very well. It's great for small projects
+but it for larger ones you'll probably want something a bit more structured.
+
+An Example Scenario
+-------------------
+
+This workflow is most suited to small projects. Here's a sample repository
+with only a single, linear series of changes:
+
+![Sample Repository Diagram](/media/images{{ parent_url }}/hg-branching-1-start.png "Sample Repository")
+
+In this example there's mostly just a single developer (you) working on the
+project to add features, fix bugs, etc.
+
+The repository is online so other people can get the code. They can add
+features and fix bugs if they want, but it doesn't happen very often because
+it's a small project.
+
+**Note:** I find it helpful to have graphs of the changesets in a repository
+so I can see what's going on. If you want some nice, quick ASCII-art graphs of
+your own repositories you can [use the graphlog extension][glog].
+
+[glog]: http://hgtip.com/tips/beginner/2009-10-03-stay-sane-with-graphlog/
+
+Branch Setup
+------------
+
+In this workflow you don't do any up-front setup at all. Just use Mercurial as
+you normally would, committing your changes along the way and pushing them to
+somewhere where other people can get them (like [BitBucket][]).
+
+[BitBucket]: http://bitbucket.org/
+
+Contributing to the Project
+---------------------------
+
+Let's say someone else starts using your project and finds a bug. They go
+ahead and fix the bug themselves and commit a changeset for the fix. They
+could then push their copy of the repository to somewhere public (like their
+own BitBucket account) and it would look something like this:
+
+![Contributor Repository Diagram](/media/images{{ parent_url }}/hg-branching-1-other.png "Contributor Repository")
+
+Once their changes are somewhere public they can email you and say:
+
+> Hey, I fixed a bug in your project.
+> 
+> The fix is changeset be3063198fea in my copy of your repository at
+> http://.../
+
+Merging Changes from Contributors
+---------------------------------
+
+When you get an email like this you would head over to their repository and
+take a look at the changeset. If you decide it's good and want to incorporate
+it into your own repository it's as simple as running `hg pull
+http://their/repo/`.
+
+If you haven't made any new changes your repository would now look exactly
+like theirs. You can update to the new tip and continue working as usual.
+
+What if you've made changes between the time they cloned (or last pulled) your
+repository and the time you read the email & pulled their changes? In that
+case your repository will look like this after you pull from them:
+
+![Sample Repository Before Merging Diagram](/media/images{{ parent_url }}/hg-branching-1-needs-merge.png "Sample Repository Before Merging")
+
+Because John's bugfix changeset and your refactoring changeset both have the
+same parent there are now two "anonymous branches" in your repository. This
+doesn't bother Mercurial at all -- repositories can have as many "anonymous
+branches" as you like.
+
+You'll probably want to merge these branches, so you'd run `hg update
+a2125cb20c54` (if you weren't already there) and then `hg merge be3063198fea`
+to merge John's bugfix with your new changes. The result would look like this:
+
+![Sample Repository After Merging Diagram](/media/images{{ parent_url }}/hg-branching-1-after-merge.png "Sample Repository After Merging")
+
+Now you're back to having just one head and you can continue working as usual,
+with John's changes and your changes all merged together.
+
+Summary
+-------
+
+This workflow is the simplest one possible. There's no up-front setup and it's
+very easy for new people to contribute to the project -- they just clone,
+commit, push, and tell you about their changes. It's great for small projects
+with one main developer and the occasional contributor.
+
+If you have a project with a lot of people working together this can get
+pretty chaotic. Your repository graph will end up looking like a tangled mess.
+In that case you'll want a workflow with a bit more structure.
+
+I'm planning on writing at least two or three more posts about some more
+complicated branching workflows in the future. If you have any specific
+examples you think I should write about please let me know!
+
+{% endblock %}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/content/blog/2010/04/a-faster-feed-apart.html	Sat May 01 00:19:01 2010 -0400
@@ -0,0 +1,276 @@
+{% extends "_post.html" %}
+
+{% hyde
+    title: "A Faster Feed Apart"
+    snip: "Rethinking A Feed Apart’s backend."
+    created: 2010-04-30 22:55:00
+    categories: ["programming"]
+%}
+
+{% block article %}
+
+[An Event Apart][aea] is a conference for web developers and designers that happens a few times a year in various cities.  [A Feed Apart][afa] is a site that aggregates tweets during each conference and displays them in a live stream so attendees can follow them during the conference, and people not attending can see what the attendees are talking about.
+
+A Feed Apart was originally written by [Nick Sergeant][nick] and [Pete Karl][pete] of [Lionburger][lionburger] during one of the conferences.  Since then a lot of people have used it and love it.
+
+The current A Feed Apart site is not without its problems.  It was written in a single night so it's not perfect.  During the last conference it went down several times and lost some tweets along the way.
+
+I work at [Dumbwaiter Design][dwaiter] with Nick and one day he mentioned that it would be cool if we rewrote A Feed Apart from the ground up.  He's learned a lot about how people use the site and what the big problems are, so we'd have a better idea of what we need to accomplish.
+
+Nick, myself and [Ali Ali][ali] have risen to the challenge and started rewriting A Feed Apart.  Ali is a designer and is taking care of the design of the new site.  Nick is handling all the frontend HTML, CSS and Javascript.  My job is the backend.
+
+Ali posted a [blog entry][aliblog] about the design of the new version so I figured I'd write about the backend to show how I'm trying to improve it.  If you have any advice I'd love to hear it -- the next An Event Apart conference is about three weeks away so there's still time to add improvements.
+
+[aea]: http://aneventapart.com/
+[afa]: http://afeedapart.com/
+[nick]: http://nicksergeant.com/
+[pete]: http://pkarl.com/
+[lionburger]: http://lionburger.com/
+[dwaiter]: http://dwaiter.com/
+[ali]: http://alialithinks.com/
+[aliblog]: http://www.dcamm.com/blog/archives/765
+
+[TOC]
+
+How AFA is Used
+---------------
+
+You can't hope to improve on a site unless you know how people are going to use it.  AFA has been around for a while and Nick has learned a lot about what people want to get out of it.
+
+There are three main kinds of users of AFA:
+
+* People attending the conference
+* People that wish they were attending the conference
+* The speakers/organizers of the conference
+
+### People Attending the Conference
+
+People that attend AEA are web developers and designers.  They're up-to-date on the latest technology and almost all of them use [Twitter][].
+
+During the conference you'll see a sea of laptops in the audience.  Attendees will tweet about whatever is happening *right now*.  There's a huge amount of conversation that happens between attendees that AFA is trying to collect and present.
+
+A simple example is one that Nick mentioned to me: if a presenter mentions a website during her presentation someone will tweet the URL so other people can have a link to easily click on.  Attendees will also tweet their agreement or disagreement with what the presenter is saying *as they're speaking*.
+
+The most important aspect of AFA for this group of people is the *real time conversation*.  If AFA doesn't show tweets until a minute after they've been posted it's useless to these people.
+
+Another important part of AFA for attendees is that it's a one-stop-shop for the conversation behind AEA.  Switching between windows/tabs/applications to read and contribute is annoying.
+
+### People That Wish They Were Attending the Conference
+
+The next group of users is those that can't attend the conference for a variety of reasons but are still interested in what's going on.
+
+For this group the real time nature of AFA is unimportant.  They're probably not going to be following the conversation 24/7 so if a tweet takes a few minutes to display it's not a big deal.
+
+What these people *do* care about is the integrity of the stream.  They don't want to miss any part of the conversation.  If AFA loses tweets they're better off doing a simple search on Twitter because they'll get the whole story.
+
+### Speakers and Organizers
+
+The last main group of users is the speakers and organizers of AEA.
+
+Any speaker worth their salt will want to know what people are said about their presentation.  Obviously they can't be reading AFA while they're presenting, but afterword they'll certainly want to go back and see what people were saying during their specific presentation.
+
+Likewise, organizers want to know which presentations people liked and which ones people didn't enjoy.  This could easily influence who they choose to invite to future conferences.
+
+For this group the most important aspect of AFA is the organization of the conversation into chunks, each of which applies to a single presentation.  By reading through each chunk of conversation they can get an idea of the general response to each presentation.
+
+[Twitter]: http://twitter.com/
+
+Goals
+-----
+
+Once we identified the main users of AFA we were able to come up with the goals for the new version of the site:
+
+* **Stay sane while developing.**  Ali, Nick and myself are rewriting the site as a side project, so we don't want it to take *too* much of our time or cause *too* much stress.
+
+* **Provide the real time conversation.**  The site needs to be fast and responsive, so people at the conferences can use it to converse.
+
+* **Don't miss anything.**  People following along from home shouldn't feel like they're missing anything.  We want to provide a *complete* version of the conversation happening behind the event.
+
+* **Organize the conversation.**  Presenters and speakers need a way to see what people are saying about them.  They want to know what people think about each "chunk" of the event.
+
+* **Grease the wheels of (physical) social interaction.**  This is something that AFA hasn't tried to address before, but that we'd like to work on with this version.  There are a lot of people at each conference and we'd like to help them get together.  Whether it's going out for dinner or meeting up at the [Media Temple][mt] party we want to get people talking to each other.  I won't talk about this goal in this post because we're still figuring out the best way to do it.
+
+[mt]: http://mediatemple.net/
+
+Staying Sane
+------------
+
+If the three of us tried to create the site with nothing more than a couple of laptops and a few chats in person we'd go crazy.  We use a few tools to help us manage the development of the site.
+
+To create **wireframes of the design** we're using a free account at [Hot Gloo][hotgloo].  Hot Gloo is a great tool that lets us quickly sketch out ideas and comment on them.
+
+To share **design comps** we're using [Dropbox][].  It's simple to set up a shared Dropbox folder and Nick and I can get real time updates when Ali makes changes to the design.
+
+To **work together on the code** Nick and I use [Mercurial][] repositories.  Mercurial lets us work on the same code bases simultaneously and we almost never have to worry about merging.  We use [Codebase][] for hosting and issue tracking.
+
+[hotgloo]: http://hotgloo.com/
+[Dropbox]: http://dropbox.com/
+[Mercurial]: http://hg-scm.org/
+[Codebase]: http://codebasehq.com/
+
+Being Real Time
+---------------
+
+The previous version of AFA wasn't *truly* real time.  When you went to the site your browser would ask AFA for the newest updates every 10 seconds.
+
+There are two main problems with this approach.  The first is that it's not really *real time*.  I've noticed this being an issue in my own experience.
+
+When I watch any of the various "live streams" of [Apple][] press conferences I'm usually at work where other people are also watching.  We very rarely load the pages of the streaming sites like Gizmodo at the exact same second, so our browsers will be out of sync with each other.  Nick might get an update that I would have to wait 8 more seconds to see.
+
+When I glance over at his screen and see an update that I don't have I instinctively refresh the page to get it.  This defeats the purpose of the "live updating" code that the developers of these sites worked on.  They may as well have just made a static page and told me to refresh.
+
+The second problem is that querying every 10 seconds can be taxing on the site's database.  We're doing this as a side project so we don't have unlimited funding for a hefty database server.  If 1,000 people are querying for updates every 10 seconds that's 100 requests per second to the database.  This means we need to have some kind of in-memory cacheing if we want the site to feel snappy on modest hardware.
+
+We want the new AFA to be truly real time.  To do this we need to use [long polling][longpolling] by users' browsers to wait for updates and return them as soon as they come in.  We also need to retrieve the updates as fast as we possibly can, and they need to be stored in memory to avoid hitting the database constantly.
+
+[Apple]: http://apple.com/
+[longpolling]: http://en.wikipedia.org/wiki/Push_technology#Long_polling
+
+### Retrieving Updates
+
+The bulk of the conversation about AEA conferences comes from Twitter.  Tweets are the most important items that we need to display on the site, so we're using Twitter's streaming API to pull them in.
+
+Since we don't want to tie up an entire server to pull in tweets I've decided to create a [Diesel][]-based application called **The Nozzletron** to parse the streaming API.  Diesel is a [Python][] framework that takes an elegant approach to asynchronous communication with clients and servers.
+
+Twitter's streaming API accepts HTTP requests and returns "chunked" responses, each of which is a tweet.  Unfortunately the Diesel's built-in HTTP client doesn't handle chunked HTTP responses so I had to write some code to handle them myself.
+
+The Nozzletron will connect to Twitter's streaming API and wait for data to come in.  If there's no data to process it will relinquish the server's processor so it can do other things.  Processing a single tweet that comes in doesn't take much time, so the server is free to do other things most of the time.
+
+I've also created another application called **The Flickrtron** to pull in [Flickr][] photos.  Unfortunately Flickr doesn't have a streaming API like Twitter so I have to resort to polling Flickr's API every few minutes for new photos.  Flickr is much less of a real time medium than Twitter though, so I don't think this is a very big problem.
+
+I'm using a Python library called [Beej's Flickr API][flickrapi] to talk to Flickr.  It is horrible.  It calls itself an "API" but is really just a thin wrapper around calls to the Flickr API.  The objects it returns for API calls are Elementtree objects representing the XML of the response.
+
+I wish I could do something like:
+
+    :::python
+    thumb_url = photo.thumbnail_url
+
+Instead I have to use this monstrosity:
+
+    :::python
+    thumb_url = "http://farm%s.static.flickr.com/%s/%s_%s_s.jpg" % (
+        photo.get('farm'), photo.get('server'), photo.get('id'),
+        photo.get('secret'),
+    )
+
+I wish there were a better Python Flickr API out there but there doesn't seem to be one.  If I've missed it please let me know!
+
+[Diesel]: http://dieselweb.org/
+[Python]: http://python.org/
+[Flickr]: http://flickr.com/
+[flickrapi]: http://stuvel.eu/projects/flickrapi
+
+### Storing Updates
+
+We need a fast way to store and retrieve updates so AFA can provide a real time view of the conversation happening at AEA.  With this in mind I've chosen [Redis][] to store the updates of the currently-happening event.
+
+Redis is an easy-to-set-up, disgustingly-fast, in-memory data store.  It's similar to [memcached][] but has more intelligent data structures that make my life easier for this project.
+
+As updates (tweets and flickr photos) are scraped by The Nozzletron and The Flickrtron they're placed at the tail end of a Redis list.  That means I can quickly and easily get all the items since item `N` when a user's browser requests them with a single `LRANGE {list-key} N -1` call.
+
+I'm also keeping a few other pieces of information in Redis.  For example, there's a set of photo IDs held in the `{item-key}:flickrtron:grabbed_photos` key that keeps track of all of the photos we've already seen.
+
+This makes it easy to tell if we've already seen a photo (and therefore don't need to query Flickr for more information) -- it's a simple `SISMEMBER {item-key}:flickrtron:grabbed_photos {photo-id}` call.
+
+I'm also using Redis to store statistics about the site like:
+
+* How many tweets we've scraped.
+* How many photos we've scraped.
+* How many people are currently waiting for updates.
+
+This kind of information will be extremely valuable in the future when we're planning improvements to the site.  Redis makes it fast and safe to update this information using the `INCRBY` and `DECR` commands.
+
+There's one more component to The Nozzletron and The Flickrton that I haven't mentioned.  Both use Redis' `PUBLISH` command to push new updates out to users' browsers as they arrive.  I'll talk more about that in the next section.
+
+[Redis]: http://code.google.com/p/redis/
+[memcached]: http://memcached.org/
+
+### Sending Updates to Users
+
+As I mentioned before we want to send updates to users *as soon as they're received*.  To do this I've created another Diesel-based application called **Halley** to handle this [Comet][]-style communication.
+
+Halley has a few components.  The first uses Diesel's [Redis API][dieselredis] to subscribe to a Redis channel like `live:{event-id}:items` and [fire][dieselfire] off messages whenever something new comes in.  As soon as a new update comes in from The Nozzletron or The Flickrtron all of Halley's clients will get it.
+
+When I started working on AFA Diesel's Redis client didn't support the very new `PUBLISH`/`SUBSCRIBE`/`UNSUBSCRIBE` commands.  I implemented them, put my changes on [BitBucket][], sent a pull request, and started talking to the Diesel crew on IRC.
+
+One of the maintainers pulled my patches and made them even better.  Now Diesel's Redis client has full `PUBLISH`/`SUBSCRIBE`/`UNSUBSCRIBE` support.  It's a great example of how open source projects can produce awesome results.
+
+The other main component of Halley is an HTTP server that listens for connections from browsers.  The Javascript on the site will call Halley and say "I need updates since number `X`", where `X` is the length of the Redis list of updates at the last time it spoke to the server.
+
+Halley uses Diesel's [HTTP Server][dieselhttp] to manage these requests.
+
+If a client is asking for everything since `X` and `X` is a smaller number than the current number of items it will return the updates that have happened since then.  This might happen if we return an update and then two more updates happen before the browser gets around to sending another request.
+
+If a client is asking for everything since `X` and `X` is equal to the current number of items Halley will wait for a new message to be fired from the [Loop][dieselloop] that's watching the Redis channel.  While Halley waits she relinquishes the processor to the server so other requests can be handled.
+
+There's a bit of code to prevent DoS attacks that request every item in the queue over and over again, of course.
+
+[Comet]: http://en.wikipedia.org/wiki/Comet_(programming)
+[dieselredis]: http://bitbucket.org/boomplex/diesel/src/tip/diesel/protocols/redis.py
+[dieselfire]: http://bitbucket.org/boomplex/diesel/src/tip/diesel/core.py#cl-90
+[BitBucket]: http://bitbucket.org/
+[dieselhttp]: http://bitbucket.org/boomplex/diesel/src/tip/diesel/protocols/http.py#cl-156
+[dieselloop]: http://bitbucket.org/boomplex/diesel/src/tip/diesel/core.py#cl-179
+
+Organizing the Conversation
+---------------------------
+
+The live stream is an important component of AFA, but it's not the only one.  We also need to organize updates into logical chunks by event and presentation, and provide archives of old events so people can see what happened.
+
+The main AFA site is built with [Django][] and served with [Gunicorn][] and [Nginx][].  It uses a [Postgresql][] database to store data that's not "live".  Because queries for live data are handled with Diesel and Redis we don't need to send those request through the full Django/Postgresql stack.  Django and Postgresql are only involved when you load a fresh page, and they're *more* than capable of handling the amount of traffic that AFA gets for those kind of requests.
+
+I've created an application called **The Strainer** to copy data from the live stream to the Postgresql database.  The Strainer looks at the list of live items in Redis, parses those items into Django models and saves them to the Postgresql database.
+
+Using two different types of stores (Redis and Postgresql) means we can get the best of both worlds for AFA:
+
+* We can keep the live data that's accessed *constantly* in an in-memory Redis datastore which makes it blazingly fast.
+
+* We can keep the less-frequently-used data in an on-disk Postgresql database which lets us to keep our memory usage low and hosting costs down.
+
+Django's models and managers make it very easy to separate updates out into the various sessions and presentations that happen at the conferences.
+
+Sessions and presentations are much less in-demand than the live stream so we can take advantage of Django's abstractions without worrying about the extra memory/CPU usage we incur by doing so.
+
+[Django]: http://djangoproject.com/
+[Gunicorn]: http://gunicorn.org/
+[Nginx]: http://nginx.org/
+[Postgresql]: http://www.postgresql.org/
+
+Staying Consistent
+------------------
+
+Another problem AFA has faced in the past is losing tweets.  No code is perfect (and mine *certainly* is not) so we need to anticipate that some of the applications we're using will crash at some point.
+
+I'm using [Supervisord][] to monitor the various processes of AFA on the server.  If a process crashes for some reason it will be restarted automatically.
+
+Supervisord also has a wonderful Python API, so I've created a simple Dashboard view in the Django site that lets us stop/start/restart each individual process with a simple web interface. The dashboard also shows us the current memory usage of the server and some other statistics so we can monitor how things are working through a web browser (instead of SSH'ing into the server, which is a pain on a phone).
+
+[Supervisord]: http://supervisord.org/
+
+Getting Bigger
+--------------
+
+An Event Apart is a large event, but it's not a *huge* event.  Despite this I've been trying to build the backend in a way that can be easily scaled.
+
+Right now it's hosted on a single server, but each of the individual components could be moved to a separate server with less than an hour of work each:
+
+* Redis
+* Postgresql
+* Django, Nginx and Gunicorn
+* The Nozzletron
+* The Flickrtron
+* The Strainer
+* Halley
+
+Moving Django/Gunicorn, Nginx, Redis, Halley, and Postgresql to dedicated servers would increase the performance of the site *immensely*.  I can't imagine an event that would provide enough traffic to require more than that.
+
+Even if there were an event that needed that kind of throughput, we could easily split the single Halley and Redis servers into multiple load-balanced servers.
+
+A Work in Progress
+------------------
+
+This new version of A Feed Apart is still being built.  I'm learning new things every time I work on it, and I'm sure there's still room for improvement.
+
+If you have any questions, advice, or want me to go more in-depth about a specific aspect of the site's backend please let me know!
+
+{% endblock %}
--- a/layout/_post.html	Wed Mar 03 10:28:53 2010 -0500
+++ b/layout/_post.html	Sat May 01 00:19:01 2010 -0400
@@ -25,7 +25,7 @@
         </p>
     </div>
     
-    <div id="leaf-content">
+    <div id="leaf-content" class="{% block article_class %}{% endblock %}">
         {% filter typogrify %}
             {% article %}
                 {% filter typogrify %}
--- a/media/css/base.less	Wed Mar 03 10:28:53 2010 -0500
+++ b/media/css/base.less	Sat May 01 00:19:01 2010 -0400
@@ -258,13 +258,13 @@
         margin-bottom: 1.5em;
         margin-right: 1.5em;
     }
-    div.with-diagrams img {
-        display: block;
-        margin-left: auto;
-        margin-right: auto;
-        background: none;
-        border: none;
-    }
+}
+div#leaf-content.with-diagrams img {
+    display: block;
+    margin-left: auto;
+    margin-right: auto;
+    background: none;
+    border: none;
 }
 
 /* Comment styles. */
Binary file media/images/blog/2010/02/hg-branching-1-after-merge.png has changed
Binary file media/images/blog/2010/02/hg-branching-1-needs-merge.png has changed
Binary file media/images/blog/2010/02/hg-branching-1-other.png has changed
Binary file media/images/blog/2010/02/hg-branching-1-start.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/requirements.txt	Sat May 01 00:19:01 2010 -0400
@@ -0,0 +1,10 @@
+-e svn+http://code.djangoproject.com/svn/django/branches/releases/1.1.X/#egg=django
+-e git://github.com/bitprophet/fabric.git@0.9rc2#egg=fabric
+-e svn+http://typogrify.googlecode.com/svn/trunk/#egg=typogrify
+-e svn+http://svn.cherrypy.org/tags/cherrypy-3.2.0rc1#egg=cherrypy
+-e git://gitorious.org/python-markdown/mainline.git#egg=markdown
+
+pyYAML
+smartypants
+Pygments
+PIL
\ No newline at end of file