In TFS 2010, one of the more significant changes that we made to our version control platform was how we identify items. Previously, version control operated in what we called “item mode” and in TFS 2010 it operates in “slot mode”. To better understand the motivation and impact of these changes, and what “item mode” and “slot mode” actually mean, I’ve decided to provide some background and detail into the changes we’ve made.
Item Mode Explained
In 2008, items are identified by an integer ItemID. Each new item added to the system received a unique integer ID that is used to refer to that item. Additionally, each item has a path, which end users of the system typically use to refer to items that they store on the server. Using this combination of ItemIDs and paths to refer to items worked in many situations, and was a design choice that enabled the rename feature (i.e. renaming an item changed its path, but not its ID). In many cases, we’ve referred to this as operating in “item mode”, that is, users act on items in the system. Items are added, edited, and renamed, and they are the same item even with a new name and modified content. The most evident place this is seen is in History, where history on, say, foo.cs will show changes made to the old name, bar.cs, because it is actually the same item.
The Switch to Slot Mode
While using ItemID and path worked in many cases, there are several situations that allow items to share a path but have differing ItemIDs. Over time, these scenarios began to complicate our code, and there were even scenarios that we couldn’t solve with this approach. This led us to change the design so that the ItemID and the path for an item are synonymous – that is, an ItemID is unique to a path. Practically, this means that when an item is renamed, it effectively becomes a new item (because it has a new path).
Unlike item mode, this concept of slot mode means that users operate on a namespace slot (i.e. $/Proj/foo.cs), not an item that exists at some path. Again, it is easy to think about this from the perspective of History. In the slot mode world, asking for history on foo.cs will only show you what has happened to files at that path. In the previous example, you wouldn’t see edits from the item when it was named bar.cs.
There were at least two primary motivating factors behind our decision to make these changes: improving the robustness of Merge, and improving the performance of the system. In some cases, merge can actually block a user trying to check-in changes, and they are forced to perform a second merge to get all of the changes. While these cases aren’t extremely common, they are severe enough that we decided we needed to change the system to make it more robust. Merge is one aspect of version control that is inherently complex, and the implementation of rename using ItemIDs further complicated the problem. Using the path as the identifier allowed us to significantly reduce complexity of the system, leading to a system that is easier to maintain, and one that performs better than the previous implementation.
Where will I see changes when using the product?
While the primary motivation was to improve merge, there are a bunch of places in the system where behaviors are changing. Here is a list of the most significant of those changes (§ indicates a change coming in Beta 2):
|Merge||Merges will always be performed in slot mode, or on the path. That is, the content of an item at a path in the source branch will be merged over to an item with the same relative path in the target branch. In cases where items are renamed in the source branch, the history for the item in the target branch will indicate where the content was merged from.
|Rename||Now implemented as a branch + delete behind the scenes (new name is branched from the old, then the old name is deleted). This allows traceability in History, but allows us to get around the problems with merging renames (see Add, Rename, Add scenario). We also added a new “source rename” bit to identify items that were deleted because they were renamed.|
|History||History will default to slot mode, that is, renames won’t be followed automatically (§). Because renames are branches behind the scenes, the history of an item before a rename will be followed exactly like a branch (§). For items that have been renamed “out” to another name, the history will have an entry that shows the “source rename” change type.|
|Changeset Details||Items that were renamed will show the “source rename” change type on the old name (§). The new name will continue to show as a “rename”.|
|Source Control Explorer||The option to show deleted items will show the old names of files and folders in addition to items that were simply deleted (§).|
A Scenario: Add, Rename, Add
Since this is all pretty complex stuff, I think scenarios are the best way to show how the system is changing. This “Add, Rename, Add” scenario, as we like to call it, is one example that a 2008 server would be blocked on that a 2010 server now handles with ease. Here’s what it looks like:
- Add a file to $/Proj/Main/File1.cs
- Branch $/Proj/Main to $/Proj/Dev
- Rename the file on the Dev branch to $/Proj/Dev/File2.cs
- Add a second file to $/Proj/Dev/File1.cs (Note that this file overlaps the old namespace slot of File2.cs)
- Merge $/Proj/Dev to $/Proj/Main
At this point, most users would expect the system to be able to correctly merge the two files back up to the Main branch, resulting in two files, File1.cs and File2.cs, that have contents that match the corresponding files in the Dev branch. However, the problem is that in 2008, all file renames were treated as conflicts when merging, so the user would have needed to accept the merge of the rename before the new file could be added in its place. Since the system can’t handle that, users were forced into merging over the rename first, and then merging over the add. Needless to say, this is less than ideal, and it’s a simple case – imagine if we had conflicting edits on the Main branch!
With the new changes we’ve made, the rename of the file on the Dev branch causes a new item, File2.cs, to be branched over to Main, and the content from the new File1.cs goes into the File1.cs on the target branch. The result is that the merge is pended successfully, no conflicts are created, and the merged can be checked in as one changeset. After the check in to Main, viewing the History for File2.cs on Main will show a change type of “merge, branch”, with the merged change coming from File2.cs on Dev (where the file was renamed from File1.cs).
The History on File1.cs on Main will show a change type of “merge, edit”, to reflect the change in the content that was merged from the Dev branch. In this screenshot, notice that changeset 26 has a change type of “add, source rename”. This is because in this example, I renamed File1.cs to File2.cs and added a new file named File1.cs in the same changeset.
Forward Compatibility for 2008 Clients
All of these platform changes in TFS 2010 have had a fairly large impact on the version control client. While we always aim to preserve forward and backwards compatibility, these changes were substantial enough that the old clients accessing 2010 servers no longer provide an optimal experience. John Nierenberg has already blogged about some of the compatibility issues on the WIT team blog, and in the future we’ll post future updates about our plans to address these issues.