TFS Version Control Concepts 2b: Namespaces in practice

Command Line usage

In Source Control Explorer, you're always operating on pending space.  At the command line, it's less clear.  Consider the following setup: you're working in a directory $/project that's mapped to c:\project and contains two files a.cs and b.cs.  For the sake of argument, we'll say they have itemIDs 1 and 2.

c:\project>tf ren b.cs c.cs
c:\project>tf ren a.cs b.cs
c:\project>tf edit $/project/b.cs

Which item gets the edit pended on it?  I actually wouldn't know without trying it, because our syntax in TFS v1 is rather haphazard: different tf commands have different rules.  Regardless, we can use our new namespace terminology to frame the possible answers.  If 'tf edit <serverpath>' operates on pending space, then the answer is item #1.  If it operates on committed space, then #2 gets checked out.  Ambiguous. 

After v1 we tried to clean up this behavior.  The new guiding rule is that local paths passed on the command line will be interpreted as pending space, while server paths will be treated as committed space.  We feel this is the best compromise between user experience and predictability. 

Users usually expect to operate on names as they see them appear on disk.  That's pending space: added/branched/undeleted files are present, deleted files are gone, and renames are in effect.  You can copy/paste paths from Explorer and it will just work.  However, you can't do everything in pending space that you can in committed space.  You can't use a pending item as the source of a Branch or Merge; you can't combine a Delete with most other pending changes; and so on.  In v1 we allowed such considerations to inspire a plethora of rules like "branch always operates on committed space" that nobody can remember.  We were also very forgiving -- for instance, we might allow 'tf <change> $/project/c.cs' or 'tf <change> a.cs' to succeed so long as there was no ambiguity.

Going forward we think it's better to throw an error every now & then if it keeps things more consistent overall.  A full review of our APIs with this principle in mind probably won't happen before Rosario, though.  Feedback is welcome on this topic!

But wait, there's another wrinkle yet.

Unsynchronized working folder mappings

c:\project>tf workfold $/project/b.cs c:\project\a.cs
c:\project>tf edit a.cs

Which item gets the edit pended on it?  Recall that until you run Get, the new mapping does not take effect on disk.  That is, a.cs and b.cs are still in their original locations.  Yet $/project/b.cs unquestionably maps to a.cs according to our rules, and the server knows this.  If we wanted to be pedantic, we'd define another namespace called "unsynchronized local space" to capture the difference between the two viewpoints.

In the end, it comes down to implementation details.  When you call Edit, the server looks at the direct item <-> local space mapping that's cached in the LocalVersion table.  This is certainly the fastest way to resolve the mapping, and it also happens to coincide with the reality on disk: c:\project\a.cs has item #1's contents at the time you made the call, and item #1 is in fact checked out. 

'tf undo' is a special case: we have to perform a Get as part of the operation anyway, so we let you specify either local path.  

Clearly, if you combine this quirk with pending renames (or worse, pending parent renames) you could get your brain in quite a twist trying to figure out the correct syntax.  In all cases, running Get will clean everything up according to the new mappings.

Case Sensitivity

Our design here is "case-sensitive storage, case-insensitive lookup."  Put another way, outputs are case-sensitive while inputs are case-insensitive.  To meet that goal, we need these rules:

  1. You cannot have two items that differ only by case.  Sorry Unix folks.
  2. Thanks to #1, we can let you refer to existing items with the wrong case w/o ambiguity.  'tf edit a.cs' and 'tf edit A.CS' are guaranteed to have the same results. 
  3. Despite #2, we always preserve the case stored on the server unless the Rename bit is set (or Add or Branch, for an item’s very first checkin).  Committed space is case-sensitive; the only way to alter it is by checking in a Rename.*
  4. When we do store a new case-sensitive name (for the first time, or during a rename), we use the name specified during the Add/Branch/Rename, not the one passed to Checkin.  In other words, pending space is case-sensitive too.  Checkin works exactly like Edit did in #2: its inputs are case-insensitively compared against pending space, yet the output is always the same.

*I think this is the first time I've used the phrase "Rename bit".  Internally, we represent the various changetypes as bitflags that can be combined in a surprising # of ways.  I'll post the complete chart sometime.  Suffice to say that for our purposes, operations like 'tf undelete /newname', or a Merge that brings renames into the target, count because they set the Rename bit.

Edge Cases

  • Consider a workspace mapping $/FairlyLongPath -> d:\short.  What happens if you create a file under 'short' whose path is MAX_PATH long?  The server-side character limit would block your checkin, and the mapping would no longer be surjective!  In reality, we don't even let you pend this kind of change.  Sorry nitpickers.

  • That's not to say you can't break my generalizations; you just have to be more clever.  Consider the following set of workspace mappings:
    $/a -> c:\a
    $/a/b/c -> c:\a\b
    At this point your workspace mappings are no longer explicit: the server has to generate an "implicit cloak" for the folder $/a/b.

  • If you think hard enough about pending space, you'll realize it's not a transformation directly on committed space.  It's really a composition of three functions: committed space => local space => [local] pending space => [server] pending space.  The middle function is the one we care about.  The 1st and 3rd functions are basically inverses of one another, so we usually ignore them.  Likewise, we can work with local paths or server paths in pending space without ambiguity.

    However, if you read the preceding section carefully, you'll see they're not perfect inverses.  The 1st transform is case-insensitive, while the 3rd is case-sensitive.  Consider what happens when you create a workspace mapping with case that differs from committed space:
    c:\project>tf workfold $/PROJECT c:\project
    c:\project>tf edit A.CS

    Committed Local Pending
    $/project/a.cs c:\project\a.cs $/PROJECT/a.cs
  • I once wrote to a customer "committed space and pending space should have the same case unless the rename bit is set."  As you can see, I was wrong :)  It's true that pending the edit using the wrong name 'A.CS' has no effect, as you'd expect from the discussion above.  However, because the transform to pending space takes a roundtrip thru your workspace mappings, incorrect case there
  • can
  • bite you.  A pending parent rename would also invalidate my statement.
  • For this customer, the fix was to alter their tool so it used case-insensitive comparisons throughout.  That's almost certainly good enough for any & all 3rd-party plugins, thanks to rule #1; if you're programming against TFS, I heartily recommend following suit.  Unless your job is actually testing TFS's correctness...