First impressions: Vault

We’ve been looking at switching source-control systems at work, and one of the new systems we’re looking at is SourceGear Vault.

I’ve played around with Vault at work, but I haven’t actually used it in a real project. So, since it’s free for a single-user installation, I decided a while back to download it at home, and use it for DGrok.

Here are my impressions so far:

Installing

Vault consists of three separate pieces: the server, the admin tool, and the client. Accordingly, they also have three installers.

But they do try to make life a little easier: the server installer is actually an “everything” installer, which you can, if you so choose, use to install all three products on the same PC.

Lovely, says I. I’m installing all three on my home computer, so I’ll just use the server installer, and select everything.

That was mistake #1.

So I logged in under an administrator account, installed all three products, logged back in with my development account, opened up the Start menu… no Vault.

Log out. Log back in as an admin. Make sure it’s really there in the Start menu. It is. Check the folder properties for the “Vault” Start menu folder. Yep, they put it under the administrator’s Start menu, instead of under the All Users Start menu.

Sigh. Uninstall. Reinstall. This time, keep a sharp eye out for the “install for all users / install for just me” radio buttons, all the while grumbling at SourceGear for not making “all users” the default.

The radio buttons aren’t there.

This really bugs me. Am I going to have to log in as admin, find the Users control panel applet, promote my development account to administrator, Fast User Switch back to the startup screen, log in as my development user, install Vault, log out, switch back to my administrator account, and remove admin privileges from my development account? Yuck. I’ve done it before, but it doesn’t exactly make me want to send the vendor a nice basket of fruit for Christmas.

Well, I thought, maybe they just didn’t put those radio buttons in the server installer. That would sort of make sense; the Web service is only directly runnable by a single user, after all. Of course, that user is actually a daemon account, not the user who’s doing the installing. So it doesn’t make a lot of sense in either case.

But I downloaded the other two installers, and uninstalled, and reinstalled the server, this time selecting only the server. Then I installed the admin tool from its own installer.

No radio buttons.

I’m getting disgusted at this point, but I decide to take the plunge, and go ahead and install the client too. And, lo! I did look, and it did have the blesséd radio buttons.

Finally.

It does mean that I’m going to have to do some jerking around any time I want to use the admin tool. But at least Vault is basically usable without going through too many stupid contortions one way or another.

Lesson learned: Don’t use the Vault server installer to install the Vault client. Just don’t do it.

Visual Studio integration

Visual Studio integration was fairly painless. And let me tell you, once you’ve got Vault going, it’s way faster than VSS.

This really surprised me. With Vault, you’ve got a .NET client piece, talking over a Web service, to an .asmx running on Microsoft IIS, which then connects to an SQL Server database. That’s a hell of a lot of overhead. Obviously, client/server has serious benefits when you’re running on a network, but since VSS just talks to flat files, I would’ve expected VSS to be much faster when everything’s on the local computer. Apparently not so.

Checkouts, in particular, are imperceptible with Vault, whereas checkouts with VSS (with the repository on my local PC here at home, mind you) always brought Visual Studio to a dead stop for one or two seconds.

Score one for Vault.

Visual Studio diffs

Vault’s diff window is one of the things I was most looking forward to. Unfortunately, there’s some poor implementation here that makes it less than a pleasure to use.

When I’m using VSS, and I want to view diffs from Visual Studio, I right-click on a file and select “Compare Versions”. There’s a brief delay, and then a VSS diff window pops right up, nicely maximized so I can see everything.

When I’m using Vault, and I right-click and select “Compare Versions”, I don’t get diffs. Instead, I get a huge dialog box with six radio buttons (one of them grayed out), edit boxes, Browse buttons, and a grayed-out checkbox. Gaah. Okay, what does it say? “Compare the working file/folder to:” The radio button for “The last version retrieved from the repository” is selected. Okay, that’s fine. Click OK.

(When VSS asks questions like this, it always — always — has a checkbox saying “Only show this dialog if I’m holding the Shift key”. I like that feature. I use that feature all the time. That’s why VSS diffs only take me two clicks. It really disturbs me that Vault doesn’t have anything like it.)

Now it pops up a diff window, but no matter how much I try, that diff window is always tiny when it pops up. It’s about half as tall as the screen, and about half as wide as the screen. Every time. So we’re talking 25% of the available real estate, and that’s not even counting overhead from things like toolbars and status bars and gutters. It makes absolutely no effort to remember the window position from one diff to the next, even within a single Visual Studio session. And nowhere can I find a preference to tell the damn thing to always maximize.

The annoyances add up. Quickly.

So, with VSS, I was always two clicks away from a usable diff. Two clicks that were pretty close together on the screen. But with Vault, I have to do those two clicks, then get my mouse over to a little OK button halfway across the screen (actually, I usually just hit ENTER with my left hand), and then move the mouse halfway across the screen again to get to the teensy little Maximize button (although I usually just double-click on the title bar, since it’s a bigger target).

When I do get that diff window up, those sub-line-level diffs are nice. But geez. C’mon, guys, get with the program:

  1. Don’t ask me what I want to compare against. You’re slowing me down. Just go ahead and use the option I selected last time. But once the diff window is open, let me change my mind; put those six options in a dropdown menu or a sidebar. (And remember my selection for next time.)
  2. And maximize the diff window by default. Always. Always.

Grokking Delphi Source

Okay, it was getting too awkward to keep referring to “my tool that searches through Delphi source“, so I’ve given it a name. Announcing… DGrok.

(So called because it groks your Delphi source code. Well, okay, it doesn’t actually become one with your code, but it does way more grokking than your typical garden-variety grep.)

I just registered www.dgrok.com. It probably won’t be live for a day or two, but it will eventually point to the new DGrok category here on my blog.

Of course, DGrok is still in development, and I don’t have even a tentative release date for the first alpha. But at least the thing has a name now.

Control.Visible… ugh.

Control.Visible lies.

I vaguely recalled this, actually, from some sessions in Reflector a few months ago. But it just bit me today. Not in a major way, but enough to be irritating.

I put my search tool’s search options into two expandable/collapsible panels, one for selecting the directories, the other for the search options. I’m not offhand aware of any good collapsible panels for WinForms, so I just rolled my own, with one control for the header and then a panel for the contents. And I showed a little “+” or “-” glyph on the header, so it would look familiar to anyone who’s used a treeview. I updated that glyph based on the Visible property of the panel, and had hooks in various places to make sure the glyph was up-to-date.

Well, this morning I noticed that, when the program came up, those glyphs were wrong. The first time I do anything to update them, they correct themselves and stay correct for the rest of the program run.

Set breakpoints. The method that updates the glyphs gets called several times during startup, and each time, the panels’ Visible property is true… up until the last time, when it’s false.

Now, I know I’m not changing the panels’ Visible property during startup. And I know that the panels are visible once the window is displayed. So… what the heck is the deal?

At least WinForms gives me a VisibleChanged event that I can hook. So I hook it, and put in a breakpoint. Lo and behold, it does indeed fire when the panels’ Visible is mysteriously changing to false. (Oddly enough, it does not fire again when their Visible is mysteriously changing back to true.) I check the stack trace, and among the code on the stack is Control.WmShowWindow.

That’s when it clicks in my head: Aha, that’s right. The value you set into Visible is not necessarily the value you get back out.

The MSDN docs have this to say:

Gets or sets a value indicating whether the control is displayed.

Wow, that’s helpful. I can tell you from looking in Reflector, however, that what happens when you read Visible is this: it returns this.Visible && Parent.Visible && Parent.Parent.Visible && Parent.Parent.Parent.Visible && …

That’s not quite how they implement it, but it makes Visible nearly useless for nearly everything. It certainly makes it useless for what I was using it for, because there are times when it returns random values (i.e., when the window is in the process of being shown, and events are getting fired, but Form.Visible is not yet true so all of its children lie).

This is not how properties are supposed to work. I want a property to return the value I set into it. Bad WinForms! No biscuit!

Once again, this is an area where Delphi got it right. You can set TControl.Visible to True, and it will return True, even if it’s on a form that’s not visible yet. They have a separate property, called Showing, that has the other “is it actually visible all the way up the chain?” behavior, for those rare occasions when that’s what you want. This seems much more in keeping with common sense.

Sigh. Not a big deal, once I identified the problem; I just added a couple more properties to my model object (ExpandDirectoryOptions and ExpandSearchOptions), and used them instead of reading the Visible property. Voila; problem solved.

Lesson learned: Consider the WinForms Control.Visible property to be write-only.

Option interaction

I’m making slow but (somewhat) steady progress on my Delphi-source-code searching program.

(I was serious, by the way: if anyone has any ideas for a good name for this tool, let me know. Otherwise, you’ll be stuck with whatever name I come up with.)

Directory recursion, complete with exclusions, is coded and ad-hoc tested. I don’t have that logic put into a thread yet, though, partly because I need to figure out how the pieces will fit together for passing the search options into the thread. In an effort to get that part straight in my head, I’ve started coding on the GUI.

The part I just finished is the search options. There are five checkboxes:

  • Whole words
    • Strict (only enabled when “Whole words” is checked)
  • Regular expressions
  • Loose whitespace
  • Ignore case

I’ve got an object that takes the search string you type in, plus these search options, and munges them together to create a Regex object. All the actual searching will be done via this regex. That makes life easier for things like “whole words”.

The transformation is interesting, though not terribly difficult, for the most part. “Ignore case” gets passed to the Regex constructor via the RegexOptions enum. “Loose whitespace” involves transforming any whitespace in the search string into \s+. “Regular expressions” passes the string through Regex.Escape(), and “Whole words” sticks a \b at the beginning of the string, and another \b at the end.

Actually, my “whole words” is a little smarter than that. \b matches the boundary between a word character (traditionally meaning “alphanumeric or underscore”, but extended in Unicode; see Char.IsLetterOrDigit) and a non-word character. That means that if I’m searching for “TFoo =“, and the search changes that to “\bTFoo =\b“, the regex will only find cases where there’s a non-word character (or the beginning of a line), followed by TFoo, a space, an equal sign, and then a word character (to establish the second word boundary). But our coding standards require another space after that equal sign, not a word character, so that regex will never match. (This is a constant source of frustration when I’m using GExperts grep; I periodically get zero search results because I forgot that “whole words” won’t work for a particular search.)

So I don’t do the naive “prepend and append \bs”, unless you check Strict. In normal “whole words” mode, I only prepend a \b if the search string starts with a word character, and I only append a \b if the search string ends with a word character. Which is generally what I mean when I leave “whole words” checked and then type something like “TFoo =“. Obviously I want a word break before the TFoo, but not after the equals. And this will figure that out. It’ll be nice to reduce the number of times I cuss out the tool for not returning any results.

Thing is, this logic doesn’t work so well when what you’re actually typing is a regex (i.e., if you check “Regular expressions”). If you type in the regex [A-Z]+, and have “whole words” checked, you’d expect to find whole words that are in all caps. (Well, assuming you also uncheck “ignore case”.) But my logic sees that the first character is [, which is not a word character, so it doesn’t prepend the \b. Same thing with the last character, +; not a word character, so it doesn’t append the \b. Net result being, it doesn’t do what you expect.

If anyone has any suggestions on how to fix this, let me know. (I’m thinking about disabling “whole words” if you check “regular expressions”. I’m not sure I like that, but I’m not sure there’s any good way around it without actually parsing the entire regex myself. Character classes, parentheses, alternation… whew. Hey, if you’re typing in a regex, you can probably type the \b too.)

The other unusual interaction was between “Regular expressions” and “Loose whitespace”. When you uncheck “Regular expressions”, the app runs your search string through Regex.Escape, which prepends all the special characters with a backslash so they’re not special anymore. But… space characters also get prepended with backslashes. Which makes my job, of finding whitespace and turning it into \s+ (a regex expression meaning “any number of whitespace characters, of any kind, be they spaces or tabs or newlines”), a good bit more difficult. If I leave that extra backslash there, and then change the space after it to \b+, I’ll be left with \\b+, meaning “a backslash followed by one or more ‘b‘ characters”. Not good.

They escape whitespace because whitespace means something special if you specify a particular flag in RegexOptions (IgnorePatternWhitespace). But you normally don’t specify that, and so you normally don’t need to escape whitespace. I looked for another version of Regex.Escape that covers this “normal” case, but I couldn’t find one.

I got around this one by iterating through all the characters in the string, letting whitespace (Char.IsWhiteSpace) pass through untouched, and calling Regex.Escape on each individual non-whitespace character. (Yes, of course I put the results into a StringBuilder.) Once I puzzled out that this was the right way to do it, it works well.

Next step: Write a thread to build the directory list, build the file list, read and parse the files, and check the regexes. Then getting the listview working properly (grumble grumble Microsoft grumble grumble not providing usable drag-and-drop out of the box grumble grumble Delphi has had fully-functional drag-and-drop since v1 grumble grumble). And then I’ll probably post an EXE if anyone wants to look at it. (The tool will still have a long way to go, but the very very basics should be at least somewhat usable.)

Excluding directories

I’m coding the directory exclusions feature of my Delphi-code-searching tool, and I wanted to get you guys’ feedback.

You’ll give the app a directory to start in, and it will automatically recurse through all subdirectories, except the ones you tell it to skip. I want this because we have some utilities in our code base that were for conversion to the current program version, so we no longer compile those tools as part of our current builds; so I don’t worry about them when I refactor, and I don’t want to search in them. One of my readers commented on wanting to exclude third-party code from searches. I think this “skip this directory” feature will be pretty useful.

I wrote the directory-recursing code this morning. The simple case (no exclusions) would basically look like this:

ArrayList arrayList = new ArrayList();
arrayList.Add(_parameters.RootDirectory);
for (int i = 0; i < arrayList.Count; ++i)
{
    DirectoryInfo thisDirectory = (DirectoryInfo) arrayList[i];
    arrayList.AddRange(thisDirectory.GetDirectories());
}

But if I want exclusions, the question arises: Where do I put that logic? There are two places that would make sense:

  • Instead of the AddRange, I could do a foreach through the GetDirectories() results, and only add directories to the ArrayList if they aren’t on the exclude list. This would mean that I would never recurse into the subdirectories of anything on the exclude list. (This is what I decided to have the code do for now.)
  • The other option would be to go from the starting directory and scan its entire directory tree, and then make a second pass to remove the excluded directories from the ArrayList. The difference here is that, if I exclude directory C:\Code\Foo, the ArrayList will still contain Foo’s subdirectories.

The difference is in whether “exclude directory X” means “ignore X and all its subdirectories”, or if it instead means “don’t process any of the files in X, but do include its subdirectories (unless I also exclude them specifically)”.

Which option do you think would be more useful? The second option gives you more fine-grained control; you can include or exclude individual directories at your discretion. But if what you really want is “exclude this entire directory tree”, then the first option would be better — unchecking all those directories one by one could be really cumbersome.

What do you think? Would you only use the first option? Only the second? Or would you really need the ability to choose, per exclusion, whether you’re skipping that directory tree or just the files in that directory?

Searching through Delphi code

I’m writing a tool that does a sort of “find in files” on Delphi source files. Yes, I know, there are zillions of such tools out there, but this one has a few extra features:

  • Ability to restrict your search only to comments, to compiler directives, to string literals, or to code. As far as I know, there are no other tools out there that have this feature.
  • Drag and drop from the search results into the Delphi editor, to open the files for editing
  • Full support for PCRE (Perl-compatible regular expressions), including positive and negative lookahead and lookbehind
  • Compound searches, e.g., “Find all files that do contain this string, but do not contain this string”
  • Lets you search within multiline comments and multiline strings (see examples below)
  • Option to search recursively but exclude certain directories
  • If you have the same filename multiple places in your source tree, will allow you to tell it that they’re really the same file (think VSS’s shared files), and only read the one with the most recent timestamp and/or shortest pathname
  • Will eventually have some degree of integration with VSS (check in, check out, undo checkout; icons change to show which files are/aren’t checked out), maybe plugin-style to allow for other SCMs
  • Will eventually let you add external tools (e.g., apps to reformat your source code), and run “check out, run the tool, check back in with an appropriate comment” as a single operation
  • Caches file contents in memory to speed future searches (but checks timestamps and reloads as needed)

An example: Suppose I have source code that looks like this.

raise EMyException.Create('This action is only available to system ' +
  'administrators and power users.');

I’ll be able to search on the text “system administrators” and get a match. Can’t do that with Find in Files, or GExperts grep, or ordinary grep. (Very useful for searching through DFMs, where you have no control over line breaks.)

Another example:

// This function will only work on Windows 9x. Windows NT
// 4.0 is not supported.

Searching on “NT 4” will get a match (if you enable the “loose whitespace” option, since it actually sees a newline between “NT” and ” 4.0″).

Another example: Suppose you have hundreds or thousands of units in your codebase, and there’s a class (TMyClass) in a commonly-used unit (call it unit MyGlobals). You want to move it into a different commonly-used unit (MyOtherGlobals). This will break any units that use TMyClass but don’t already have MyOtherGlobals in their uses clause. No problem: search for all files that do contain TMyClass, but do not contain MyOtherGlobals. Click “Check Out” to check the files out of VSS; drag them into the IDE; add MyOtherGlobals to their uses clauses; save; and click “Check In”. (Much simpler than the alternative, of just moving the class, compiling to see what breaks, and checking out and fixing one unit at a time.)

I’m writing the code in .NET, mainly because of its fabulous regex library. (Ironically enough, I’m writing this tool, whose biggest selling point is a Delphi parser, in C#. Eh, big deal; the Delphi compiler is written in C.)

I already have proof-of-concepts working for all of these features. All that’s left is to tie them all together, which is what I’m working on now. I’m writing this utility because I need it at work (where I’ve already written, and am using, some of the proof-of-concepts). I’m writing the full tool on my own time so that I own the code.

Does anyone else need a tool like this? If so, I may polish it up and release it at some point. (Also let me know if there are any features you’d like added, or if you have a killer name for the app.)