Strange production errors
The following code cause a really strange error in production:
new MailAddress("test@gmail.com");
The specified string is not in the form required for an e-mail address.
Huh?!
Obviously it is!
After immediately leaping to the conclusion that .NET is crap and I should immediately start writing my own virtual machine, I decided to dig a little deeper:
| Character | Code |
|---|---|
| t | 116 |
| e | 101 |
| s | 115 |
| t | 116 |
| @ | 64 |
| g | 103 |
| m | 109 |
| a | 97 |
| i | 105 |
| l | 108 |
| . | 46 |
| ? | 8203 |
| c | 99 |
| o | 111 |
| m | 109 |
8203 stands for U+200B or zero width space.
I guess that someone with a software testing background decided to get medieval on one of our systems.
I am turning 0x1E tomorrow
In hex, I am still a teenager
.
To celebrate that, starting from the 20 Dec all the way to the new year, I decided to offer a 30% discount on all the profilers. All you need to do is to use the following coupon code:
- 01E-45K2D46V6K
The offer is valid for:
Time Traveling emails, and async operations
The customer is always right?
When you get this sort of an email, you almost always know that this is going to be bad:
Let us start with: Which product? What license key? What order? What do you expect me to do about it?
At least he is polite.
Hm, I wonder what is going on in here…
This error can occur because of a trial that has expired or a subscription that has not been renewed.
He attached a Trial licensed to this email.
It is like a Greek tragedy, you know that at some point this is going to arrive at the scene.
I mean, we explicitly added the notion of subscriptions to handle just such cases, of people who want to use the profiler just for a few days and don’t want to pay the full version price. And you can cancel that at any time, incurring no additional charges.
Sigh…
All the Rhinos in the World, Unite!
Update: You can see all the photos here
Well, I might have gone a bit overboard, but I do like Rhinos.
Say hello to my crash of rhinos.
And here is a nice family of them:
I don’t get this way very often, but AWESOME!
Entering to the office…
And just to give you some perspective:
The Big Event is just a week away
I am getting married on the 29th May, which is just a week away. You may notice a decline in the number of posts to this blog (and general activity for work related stuff) around that time frame.
For quite some time, I had to… endure certain types of jokes about what would happen when I would get married. Given the choice between my own happiness and the desires of some members of the development community to see me in a dark room eating pizza and working at all hours, I know what I would take… ![]()
Never the less, as a consolation prize to those people, and as a way to spread my happiness, the following coupon code with give you 29% discount for all the profilers (NH Prof, EF Prof, L2S Prof, LLBLGen Prof, Hibernate Prof): WDG-45K2D467C5
The coupon works for single license purchases, and it is only valid until the 1st of June.
More on the joy of support: My trial expired!
I got the following very interesting email:
You might have noticed that I have kept the email address of the sender public. That is an important clue.
The email was sent from a public email gateway, one of those places where you have a disposable email address.
I suspect that there isn’t actually a bug, but that the system is working as planned ![]()
And there is this complaint:
Executing TortoiseGit from the command line
Originally posted at 1/6/2011
I love git, but as much as I like the command line, there are some things that are ever so much simple with a UI. Most specifically, due to my long years of using TortoiseSVN, I am very much used to the way TortoiseGit is working.
I still work from the command line a lot, and I found myself wanting to execute various actions on the UI from the command line. Luckily, it is very easy to do so with TortoiseGit. I simply wrote the following script (tgit.ps1):
param($cmd)
& "C:\Program Files\TortoiseGit\bin\TortoiseProc.exe" /command:$cmd /path:.
And now I can execute the following from the command line:
tgit log
tgit commit
And get the nice UI.
Please note that I am posting this mostly because I want to be able to look it up afterward. I am sure your git tools are superior to mine, but I like the way I am doing things, and am reluctant to change.
Google vs. Bing
Psychic Debugging
Enough is enough: iTunes got to go
Here is the story, the only reason that I am using iTunes is because I want to sync books that I buy from audible.com to my iPhone.
I am still fighting this problem. And I have installed / uninstalled, danced the mamba and even try some chicken sacrifice on the last full moon. Nothing helps, oh, it will works once, immediately after I install it, but on the next reboot, it will show the same error.
Right now I have uninstalled iTunes from my system, and I am currently building a VM specifically so I would be able to sync new audiobooks to my iPhones. I think that this is insane.
Anyone got a better option than that?
Unstable
Originally posted at 10/20/2010
Currently…
- RavenDB has an unstable fork
- Which has an unstable branch (too unstable to be the master branch of the unstable fork!)
And then there is what I am working on locally…
I hate making big changes.
The other side of chasing the money
When I started doing my own consulting, I realized that sometimes I would have to chase after a client in order to get paid. Luckily, it hasn’t happened often.
I did not expect the reverse to happen, but it did. I just had to send the following formal notice to someone who does work for me:
Hi guys!
I am pretty sure that I owe you money. Would you mind terribly if I paid you?
And yes, this post is here to serve as a kick to the people in question.
Ruination in two easy steps
How far can you push commercialization?
I was recently at a private company event (not my company, I was invited, along with others, because we have a close association to that company). The event itself wasn’t notable, but there was one thing that really bothered me, before the event actually started, there was the usual phase when everyone is munching on the snacks and mingling. The food was some sort of green cupcakes with inspirational messages on them: “think positive”, “fitting the world to you”, etc.
All in all, I found that somewhat strange, but I didn’t really care, but I was talking with a few friends when a woman walked up to us and started handing out coupons for some free demo courses using a whole new technique, etc. I was quite taken aback. I am used to stuff like that on conferences floors, where you have booth babes doing stuff like that, but that was a private meeting of less than fifty people, and I couldn’t understand what was going on.
It helped that the woman kept dropping the same phrases that appeared on the cupcakes. That was later confirmed at the beginning of the meeting, where the presenter stood up and started by thanking the sponsors for bringing the food, etc.
Looking back at this, I am both appalled, amazed and utterly unsurprised (you can be both at the same time, it seems). That company actually sold sponsorship for an internal, private, meeting. I don’t really know what was the point, if they were trying to save money on the food or they were actually making money out of this, but that behavior really bother me.
I am absolutely for commercialization, if only because the bank would otherwise object, but I was utterly stunned by how crass it was.
What is next? Hiring employees for the express purpose of watching commercials while the company is getting paid for that?
More to the point, there is some expectation about how such functions are going to be, and stunts like that are leaving very bad impression.
I ain’t going against my professional judgment pro bono
I had an interesting conversation with a guy about some problem he was having. This was just one of those “out of the blues” contacts that happen, when someone contact me to ask a question. He presented a problem that I see all too often, trying to create a system in which the entities are doing everything, and he run into problems with that (to be fair, he run into a unique set of problems with that). I gave him a list of blog posts are articles to read, suggesting the right path to go. After a few days, he replied with:
I went over your advised reading in depth, but let me describe in short the properties and functions of our system, which I think causes the system to be an exception to those methods.
He then proceed to outlay his problem, a proposed solution and then asked a very specific NHibernate question that was a blocking stumbling block to get ahead with the solution he wanted. My reply was that he took the wrong approach, a suggestion how to resolve it in a different manner and a link to our NHibernate Commercial Support option.
Contrasting UberProf & RavenDB from business perspective
I was recently asked to contrast the business decisions related to the profiler and RavenDB. I thought that it would make an excellent post.
There are a lot of aspects to thing about here, actually. The profiler is an add on tool, it is only useful if you are using one of the supported OR/Ms, but if you do… it:
- has very low barrier to entry, you need to reference the dll and add a single line of code.
- provides immediate value, you can see the benefits that it gives you.
- have very few moving parts that users can break.
NH Prof was released on Jan 1st, 2009. The first sale happened on Jan 2nd, 2009 (thanks Yann!).
The lead time for the profiler tends to be very short. Because there is very little that you need to invest and there is a lot that you gain. Yesterday I introduced a guy to the profiler as a way to help him see what his app is doing, he made a purchase about an hour later.
That is excellent news from my point of view. :-)
RavenDB, on the other hand:
- has a very high barrier to entry, not so much from technical perspective, but from adoption one.
- requires you to make significant changes to the way you work.
- takes time to show why it is beneficial.
- requires payment only when you actually goes live.
- requires much higher degree of support for users.
That means that while it takes a few minutes to decide if you want the profiler (and the rest of the 30 days trial is spent getting corporate approving it :-) ), for RavenDB the lead time until you pull out your credit card is much longer.
That has some interesting implications. I actually spent a lot more (time & money) in the profiler than I spent (outright) on RavenDB. But the major difference is what type of investment that would be.
There is a term in economics called sunk cost, that is all the costs associated with building a product up to the point you released it. That is money already spent. But what usually matter a lot more is that once you reached the release point, can the cash flow from a product justify the continued work on the product ( and maybe, at some point, pay for the product development) ?
NH Prof was a big investment for me, but money started coming in shortly afterward, and it became apparent that it was sustainable product. For RavenDB, the costs have actually been a lot lower (since the majority of them represented my own time), but the expectation is that it would take about a year or two before it would be be possible to say if RavenDB is a sustainable product.
In that sense, RavenDB represent a lot riskier investment. If RavenDB wasn’t rattling in my head for so long, I would have probably would have gone to something with much shorter lead time.
It is interesting to me to see how many factors there are in those sort of decisions. So many things to balance.
Financial analysis
I decided to spend some time with Excel trying to do a look back at how things are doing. I am not going to give you numbers, but the trends in the data are interesting. All the data is relevant to this year only.
Tell me that the top one doesn’t remind you of a rude gesture. As you can see, NHibernate rules the roost here, on the bottom, it is even clearer. I think that a lot of that is that I am closely related to NHibernate that is making the difference.
Looking at the data over time gives me a very interesting perspective about the introduction of subscriptions. That was expected, but I don’t think that I really grokked that. Looking at the bottom image, I can tell you that subscriptions are pretty big, from the point of view of license numbers, but they are much weaker from the point of view of money in the bank.I knew that, I even wanted that, since the whole point of subscriptions was to get sustainable revenue stream rather than money today. But I wasn’t really ready for that.
This one is particularly annoying, because there is a dry spot around the 20th every single month, and I get abandonment anxiety at that time.
That is it for now :-)
Why am I tired? Answer for week of 1 – 6 Aug, 2010
- Blog:
- 23 posts written
- 258 comments.
- 32 of said comments by me.
- RavenDB documentation: 17 entries created / updated.
- RavenDB: 46 commits.
- NH Prof: 31 commits.
- Email: Over 600 threads (a lot more in actual emails).
My head feel like it has been overheating for a while. Time to quit this infernal machine and watch some TV, maybe I’ll lose some IQ points.
iTunes full screen movies incompatible with Large Font sizes?
I have a PC hooked to my TV, but the problem is that there seems to be a bug in iTunes, when I set the font site to be large enough to actually be readable, like so:
I lose the ability to view full screen movies in iTunes, when I switch the movie to full screen, it continues playing (I can hear it) but the display switch back to the iTunes library, rather than the movie.
I verified the resetting the font size fixes this problem, and this is in iTunes 9.2.1.
Anyone run into this? Any solutions?
The cost of money
This is just some rambling about the way the economy works, it has nothing to do with tech or programming. I just had to sit down recently and do the math, and I am pretty annoyed by it.
The best description of how the economy works that I ever heard was in a Terry Prachett’s book, it is called Captain Vimes’ Boots’ Theory of Money. Stated simply, it goes like this.
A good pair of boots costs 50$, and they last for 10 years and keep your feet warm. A bad pair of boots costs 10$ and last only a year or two. After 10 years, the poor boots cost twice as much as the good boots, and your feet are still cold!
The sad part about that is that this theory is quite true. Let me outline two real world examples (from Israel, numbers are in Shekels).
Buying a car is expensive, so a lot of people opts for a leasing option. Here are the numbers associated with this (real world numbers):
| Buying car outright | Leasing | |
| Upfront payment | 120,000 | 42,094.31 |
| Monthly payment (36 payments) | 0 | 1,435.32 |
| Buying the car (after 3 yrs) [optional] | 0 | 52,039.67 |
The nice part of going with a leasing contract is that you need so much less upfront money, and the payments are pretty low. The problem starts when you try to compare costs on more than just how much money you are paying out of pocket. We only have to spent a third.
Let us see what is going to happen in three years time, when we wan to switch to a new car.
| Buying car outright | Leasing | |
| Upfront payment | 120,000.00 | 42,094.31 |
| Total payments | 0.00 | 51,671.52 |
| Selling the car | -80,000.00 | 0.00 |
| Total cost | 40,000.00 | 93,765.83 |
With the upfront payment, we can actually sell the car to recoup some of our initial investment. With the leasing option, at the end of the three years, you are out 93,765.83 and have nothing to show for it.
Total cost of ownership for the leasing option is over twice as much as the upfront payment option.
Buying an apartment is usually one of the biggest expenses that most people do in their life. The cost of an apartment/house in Israel is typically over a decade of a person’ salary. Israel’s real estate is in a funky state at the moment, being one of the only places in the world where the prices keep going up. Here are some real numbers:
- Avg. salary in Israel: 8,611
- Avg. price of an apartment (in central Israel): 1,071,900
It isn’t surprising that most people requires a mortgage to buy a place to live.
Let us say that we are talking about a 1,000,000 price, just to make the math simpler, and that we have 400,000 available for the down payment. Let us further say that we got a good interest rate of the 600,000 mortgage of 2% (if you take more than 60% of the money you are penalized with higher interest rate in Israel).
Assuming fixed interest rate and no inflation, you will need to pay 3,035 for 20 years. But a 2% interest rate looks pretty good, right? It sounds pretty low.
Except over 20 years, you’ll actually pay: 728,400 back on your 600,000 loan, which means that the bank get 128,400 more than it gave you.
The bank gets back 21.4% more money. With a more realistic 3% interest rate, you’ll pay back 33% more over the lifetime of the loan. And that is ignoring inflation. Assume (pretty low) 2% per year, you would pay 49% more to the bank in 2% interest rate and 65% more in 3% interest rate.
Just for the fun factor, let us say that you rent, instead. And assume further that you rent for the same price of the monthly mortgage payment. We get:
| Mortgage | Rent | |
| Upfront payment | 400,000.00 | 0.00 |
| Monthly payment | 3,000.00 | 3,000.00 |
| Total payments (20 years) | 720,000.00 | 720,000.00 |
| Total money out | 1,120,000.00 | 720,000.00 |
| House value | 1,000,000.00 | 0.00 |
| Total cost | 120,000.00 | 720,000.00 |
After 20 years, renting cost 720,000. Buying a house costs 120,000. And yes, I am ignoring a lot of factors here, that is intentional. This isn’t a buy vs. rent column, it is a cost of money post.
But after spending all this time doing the numbers, it all comes back to Vimes’ Boots theory of money.
Building a managed persistent, transactional, queue
When one approaches building transactional systems, there are two main ways one can approach them.
- Transaction log
- Append only
In both cases, we rely on very important property, fsync. fsync is actually a unix call, which flushes all data to the actual device. In essence, this means “write the the actual hardware and don’t return until the hardware confirmed that the data was saved”. In Windows, the equivalent call is: FlushFileBuffers, or WriteThrough. WriteThrough is better if you need to call FlushFileBuffers every single time, while FlushFileBuffers is significantly faster if you only need to call it once in a while.
FileStream.Flush(true) in .NET 4.0 translate to FlushFileBuffers.
Transaction log systems tend to be more complex than append only systems, but append only systems use more space. Space tend to be pretty cheap, so that is a good tradeoff, I think.
Given that, let us see how we can consider a managed transactional persistent queue. One interesting aspect is that append only, just like immutable data structures, is really not friendly for things like queues. The problem is that adding an item to the queue requires modification to the entire queue, which result in a large number of memory allocations / writes to disk.
The functional crowd has solved the problem long ago, it seems. In essence, a functional queue is composed of two immutable lists, one for the front of the queue and the other for the back of the queue. You add things to the back of the queue, and pop things from the front of the queue. When the front of the queue is empty, you set the front of the queue to be the reversed back of the queue and clear the back. The link should make it clear how this works.
Our first task is actually implementing the list. Since we really only need it to be a stack, I won’t bother with list functionality and just implement a very simple stack. With that, we can implement the actual stack:
public class Stack { private readonly Stream reader; private readonly Stream writer; private StackItem current; private readonly BinaryWriterWith7BitEncoding binaryWriter; private readonly BinaryReaderWith7BitEncoding binaryReader; private readonly List<StackItem> unwritten = new List<StackItem>(); private class StackItem { public long Position { get; set; } public readonly StackItem NextItem; public readonly long Value; public readonly long? Next; public StackItem(long value, StackItem nextItem) { Value = value; NextItem = nextItem; } public StackItem(long value, long? next) { Value = value; Next = next; } } public long? CurrentPosition { get { return current == null ? (long?)null : current.Position; } } public Stack(Stream reader, Stream writer, StartMode mode) { this.reader = reader; this.writer = writer; binaryReader = new BinaryReaderWith7BitEncoding(reader); binaryWriter = new BinaryWriterWith7BitEncoding(writer); if (mode != StartMode.Open) return; current = ReadStackItem(reader.Position); } public void Push(byte[] data) { var pos = writer.Position; binaryWriter.Write7BitEncodedInt(data.Length); binaryWriter.Write(data); PushInternal(pos); } private void PushInternal(long pos) { current = new StackItem(pos, current); unwritten.Add(current); } public byte[] Pop() { var result = PopInternal(ref current); if (result == null) return null; reader.Position = result.Value; var size = binaryReader.Read7BitEncodedInt(); var bytes = binaryReader.ReadBytes(size); return bytes; } private long? PopInternal(ref StackItem item) { if (item == null) return null; unwritten.Remove(item); var result = item.Value; if (item.NextItem != null) item = item.NextItem; else if (item.Next != null) item = ReadStackItem(item.Next.Value); else item = null; return result; } public void Flush() { foreach (var stackItem in unwritten) { stackItem.Position = writer.Position; binaryWriter.WriteBitEncodedNullableInt64(stackItem.Value); binaryWriter.WriteBitEncodedNullableInt64(stackItem.NextItem != null ? stackItem.NextItem.Position : stackItem.Next); } unwritten.Clear(); } private StackItem ReadStackItem(long position) { reader.Position = position; return new StackItem( binaryReader.ReadBitEncodedNullableInt64().Value, binaryReader.ReadBitEncodedNullableInt64() ) { Position = position }; } public Stack Reverse() { var stack = new Stack(reader, writer, StartMode.Create); var item = current; while(item != null) { var value = PopInternal(ref item); if(value!=null) stack.PushInternal(value.Value); } return stack; } }
The code make several assumptions:
- The stream is using WriteThrough, so once a write was completed, it is saved to the disk.
- It is not our responsibility to keep track of things like the current position, this is handled by higher level code.
- We are allowed to jump around on the reader, but the writer is only doing appends.
Given the stack behavior, we can now implement the queue:
public class Queue { private readonly Stream reader; private readonly Stream writer; private Stack front; private Stack back; private readonly BinaryReaderWith7BitEncoding binaryReader; private readonly BinaryWriterWith7BitEncoding binaryWriter; public Queue(Stream reader, Stream writer, StartMode mode) { this.reader = reader; this.writer = writer; binaryReader = new BinaryReaderWith7BitEncoding(reader); binaryWriter = new BinaryWriterWith7BitEncoding(writer); switch (mode) { case StartMode.Open: ReadStacks(); break; case StartMode.Create: InitializeEmptyStacks(); break; } } private void InitializeEmptyStacks() { front = new Stack(reader, writer, StartMode.Create); back = new Stack(reader, writer, StartMode.Create); } private void ReadStacks() { var frontPos = binaryReader.ReadBitEncodedNullableInt64(); var backPos = binaryReader.ReadBitEncodedNullableInt64(); if (frontPos != null) { reader.Position = frontPos.Value; front = new Stack(reader, writer, StartMode.Open); } else { front = new Stack(reader, writer, StartMode.Create); } if (backPos != null) { reader.Position = backPos.Value; back = new Stack(reader, writer, StartMode.Open); } else { back = new Stack(reader, writer, StartMode.Create); } } public void Enqueue(byte[] data) { back.Push(data); } public byte[] Dequeue() { var result = front.Pop(); if (result != null) return result; front = back.Reverse(); back = new Stack(reader, writer, StartMode.Create); return front.Pop(); } public void Flush() { front.Flush(); back.Flush(); QueuePosition = writer.Position; binaryWriter.WriteBitEncodedNullableInt64(front.CurrentPosition); binaryWriter.WriteBitEncodedNullableInt64(back.CurrentPosition); } public long QueuePosition { get; private set; } }
Now, just to make things interesting, let us see what it actually means:
var sp = Stopwatch.StartNew(); using (var writer = new FileStream("data", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Delete | FileShare.Read, 16 * 1024, FileOptions.SequentialScan)) using (var reader = new FileStream("data", FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, 16 * 1024, FileOptions.RandomAccess)) { Queue queue = new Queue(reader, writer, StartMode.Create); var bytes = new byte[1024*7]; new Random().NextBytes(bytes); for (int i = 0; i < 100000; i++) { queue.Enqueue(bytes); } queue.Flush(); writer.Flush(true); } Console.WriteLine(sp.ElapsedMilliseconds);
And this completes is a bit over 14 seconds, or over seven thousands (pretty big) items per second.
Historical Mondial Moments
It sometimes seems that the whole world goes mad over this thing, which I can’t really understand. But every time that I hear about the Mondial, it bring tears to my eyes, as I remember…
It was 2002, and the Mondial took place somewhere in Asia, which meant that the games were played in the morning, in Israel’s time. A lot of people in Israel are a bit touched in the head regarding soccer, so that cause quite a bit of disturbance all around, since people didn’t show up for work because they wanted to watch the games.
Anyway, I was minding my own business in Prison Six, just doing my usual rounds. Okay, calm down, you didn’t misread the last sentence, I was a prison guard at the time. One of the procedures for working in a prison is never work alone, we always worked in pairs. One of us would go into a cell to handle something, while the other remained outside the cell’s door. The idea was that it would prevent potential kidnapping.
The prison was very conscious of that since there was a major prison break / prison guard kidnapping / hostages just 5 years previously.
Where was I? Oh, yeah. It was Mondial time, and we put a lot of the inmates in the club, so they could watch the game. I was checking that the cell was clean for inspection, so I was pretty deep inside, where they was a giant roar from the club.
My partner was an avid soccer fan, so he didn’t think twice (actually, knowing the guy, thinking once was too much), he slammed shut the cell’s door and run to watch the replay.
I wouldn’t have minded, except that he locked me in the cell with 7 inmates.
The key for handling the situation was to pretend that I didn’t notice that and hope that the inmates wouldn’t either. By the time my partner got back to me, that cell was clean.
Deleting hard links problem
This code fails:
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern bool CreateHardLink(string lpFileName, string lpExistingFileName, IntPtr lpSecurityAttributes); static void Main() { var writer = File.CreateText("test.txt"); writer.Write("test"); writer.Flush(); CreateHardLink("hard_link.txt", "test.txt", IntPtr.Zero); File.Delete("hard_link.txt"); }
The problem here is that the hard_link.txt file is considered to be open. What I would expect to happen is that the delete would succeed in removing the directory entry, but keep the file. In other words, I would only expect it to fail if this is the last directory entry for the file (resulting in an actual delete).
For now I changed this to try to delete, but on failure, mark the file to be deletes on the next reboot.
Extracting a list of committers from Git
There doesn’t seem to be a nice way to getting stats like “who are the committers in this project” from Git. There is probably some fancy script that does sed/awk magic to do so, but I went with a simpler alternative:
git log --raw > log.txt
var logFile = @"C:\work\ravendb\log.txt"; var committers = from line in File.ReadAllLines(logFile) where line.StartsWith("Author: ") let author = line.Substring("Author: ".Length) group author by author into g let result = new {Committer = g.Key, CommitsCount = g.Count()} orderby result.CommitsCount descending select result; foreach (var committer in committers) { Console.WriteLine(committer); }
Running this on Raven’s code produces:
{ Committer = Ayende Rahien <ayende>, CommitsCount = 555 }
{ Committer = Ayende Rahien <Ayende>, CommitsCount = 477 }
{ Committer = unknown <Ayende, CommitsCount = 72 }
{ Committer = Paul B <github, CommitsCount = 24 }
{ Committer = Andrew Stewart <Andrew.Stewart, CommitsCount = 24 }
{ Committer = Benny Thomas <jan.thomas, CommitsCount = 19 }
{ Committer = Luke Hertert <lukehertert, CommitsCount = 15 }
{ Committer = Bobby Johnson <bobby.johnson, CommitsCount = 13 }
{ Committer = Rob Ashton <robashton, CommitsCount = 11 }
{ Committer = unknown <LukeHertert, CommitsCount = 7 }
{ Committer = Andrew Theken <theken.1, CommitsCount = 6 }
{ Committer = AndyStewart <Andy.Stewart, CommitsCount = 3 }
{ Committer = Steve Strong <steve, CommitsCount = 3 }
{ Committer = unknown <Aaron Weiker, CommitsCount = 1 }
{ Committer = Emil Cardell <emil.cardell, CommitsCount = 1 }
{ Committer = bjarte.skogoy <bjarte.skogoy, CommitsCount = 1 }
{ Committer = unknown <henke, CommitsCount = 1 }