Archive for the ‘Version Control’ Category

Get-TfsItemProperty is the bridge between server and local objects

Friday, March 27th, 2009

Most of the real-world examples of the TFS powershell tool revolve around querying (and possibly updating) the state of the server. 

And rightfully so: most of the operations you want to do locally are already possible without any special tools.  The “Powershell snap-in system” we know and love today was created to help IT admins manage their servers – even today, only a brave few developers have completely replaced cmd.exe at their daily workstation.

Nevertheless, we shouldn’t be satisfied to have our TFS cmdlets live in their own little world.  Luckily, all the info we could ever want about a TFS version controlled item – including its local state – comes packaged in the ExtendedItem object.  Even more fortuitously, if you revisit the exploration of QualifiedItemSpec with an eye for detail, you’ll notice that one overload of ToQualifiedItem() is not like the others.*  In the constructor where ExtendedItem decays into QualifiedItem, the properties saved into the resulting tuple include LocalItem and VersionLocal instead of more familiar ones like ServerItem.  This is not a coincidence :) 

Let’s sum up what we know so far.  Native TFS objects are funneled down the pipeline by stripping them to bare essentials about the items they represent (QIs); these serve as a common interface between cmdlets.  When an ExtendedItem is stripped down, the next cmdlet will use the local filename & local version info as its parameters.  Get-TfsItemProperty is the cmdlet that returns ExtendedItems.  Capiche?

Time to put our newfound knowledge into action.  If you’ve also been following the last several posts on file manipulation in powershell, these examples will hopefully inspire an “aha!” moment.

001
002
003
004
005
# find all C# makefiles with HintPath elements, check them out, and open them in my text editor
tfprop $/project/*csproj -r | get-fullpath | ss hintpath | tfedit -passthru | ue

# find my shelvesets related to powershell, unshelve them, and open every file in the ISE
get-tfsshelveset | ? { $_.name -like "*powershell*" } | tfunshelve | tfprop | ise

[Note that I have get-tfsitemproperty aliased to ‘tfprop’ rather than ‘tfproperties’ as it appears in a default install of the Power Tools.]

Are we having fun yet?!

So far we’ve only really exercised the filename part of the QI.  Equally interesting things happen when you take advantage of the special ability that Get-TfsItemProperty offers to manipulate the local version.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
# Force unshelve to pend changes against your current workspace ("have") version. Ordinarily,
# unshelve rolls the affected files back to the version you had at the time you shelved the
# changes.
# Pro: this ensures your workspace stays in a consistent, buildable state
# Con: you may need to resolve content and/or namespace conflicts
function unpack
{
    [CmdletBinding()]
    param (
        [parameter(Mandatory=$True, Position=0, ValueFromPipeline=$True)]
        [Microsoft.TeamFoundation.PowerTools.Powershell.ShelvesetSpec]$ss
    )
   
    process
    {
        $currentWorkspaceVersions = tfstatus -shelveset $ss | tfprop
        restore-tfsshelveset $ss | out-null
        $currentWorkspaceVersions | tfget
        tf resolve /prompt
    }
}

[Microsofties: think bbpack / jjpack]

Opens up a whole new world of TFVC scripting mania, wouldn’t ya say?

*I’ll admit the one for GettingEventArgs is a little funky as well.  Here, the choice of properties to expose came down to UX / gut feel.  TargetLocalItem is usually what you want to see; it tends to be the “final result” of mainline operations, surviving things like Rename and Undo fairly well.  But it’s not always the best choice.  Obviously when it’s null we need a backup; good old ServerItem will do.  It’s [almost always] guaranteed to be present, but it’s  not always perfect either – now you have the occasional strange appearance of server paths in your list of local files being touched by the Update.  Finding the right info gets really tricky really fast: was TargetLocalItem null because of a pending Delete, because it’s cloaked or otherwise excluded from our workspace, or because we’re undoing a pending Branch or Merge?  That’s just one example.  The logic behind which name tf.exe displays in which scenario is actually one of the more complex pieces of the app.  With powershell we can accept “good enough” since the object model lets you manipulate things to your heart’s content.

Fixing consistency issues with Powershell’s filename properties

Friday, March 27th, 2009

When I decided to bitch about select-object, I used “dir” in my examples for a reason: passing filesystem objects around the pipeline is simultaneously one of the most common yet one of the most annoying things you can do in Powershell.  Let’s review some built-in cmdlets from the POV of getting the full path to a given file or folder:

CmdletInput property name accepted on pipelineOutput property name on object sent to pipeline
Get-Item, Get-ChildItem, Set-ItemPathFullName
Join-Path, Split-Path, Test-PathPathn/a
Resolve-Path, Select-StringPathPath*
New-ItemNameFullName

* note that the built-in documentation for Resolve-Path is wrong!  as of CTP3, it’s claiming the output is a string

Plus, you may be working with 3rd party tools that obey none of the above conventions. 

For example, with the TFS Power Tools, Hyung had the foresight to add string[] to the list of types we can convert to QualifiedItems behind the scenes using extra constructors, but we still don’t support the ValueFromPipelineByPropertyName idiom that’s needed to make non-trivial filesystem objects flow.  Furthermore, the objects we output are native TFS types, which were never designed with Powershell in mind.  Most of the time there’s little overlap between these objects and non-TFS cmdlets, but a big exception is the LocalItem property of the ExtendedItem object returned by “tfprop.”  In hindsight, we should have added a NoteProperty that aliased it to Path.  (…or FullName, or Name, depending on whether the PS guys make up their mind :) )

Then there are tools that have nothing at all to do with Powershell.  Some sort of wrapper script is going to be required regardless, but without consistency in the kind of input your wrapper can expect, it can get needlessly complex.

My solution to all of the above is outlined below.  Time to let the code do the talking:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
function get-fullpath
{
    [CmdletBinding()]
    Param(
        [Parameter(ParameterSetName="ByPropertyName", Position=0, Mandatory=$true, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true)]       
        [Alias("FullName")]
        [Alias("FullPath")]
        # [Alias("Name")] - too many false positives
        [Alias("Path")]
        [Alias("LocalItem")]
        [string] $file
    )
   
    Process
    {       
        # eliminate nonexistent files, obviously, but also random objects that were coerced from the pipeline by calling their ToString() method
        if (!(test-path $file))
            { break }
       
        # expand relative paths (in case the immediate window's workingdir != the workingdir of powershell_ise.exe)
        resolve-path $file | 
            # and also make sure UNC paths are in normal format, not Powershell format
            convert-path 
    }
}

Who knew that ValueFromPipelineByPropertyName worked on aliases?  I didn’t.  I thought it would be nice, but since I didn’t see it documented anywhere, my initial sketch for this function used a ton of ugly ParameterSets as a workaround.  Somewhere in there I decided to try Aliases; lo and behold, it works!  Yay.

This function is useful on its own – if nothing else I use it as a wholesale replacement for resolve-path, whose behavior on UNC paths is bizarre in my opinion – but its real value is as a helper.  For example, a couple snippets from my $profile:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
function tfedit 
{
[CmdletBinding()]
    Param(
        [Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)]
        [psobject] $file,
        [Parameter()]
        [switch] $PassThru = $false 
    )
   
    Process 
    {
        if ($PassThru)
            { $file }
       tfpend -edit ($file | get-fullpath)
    }
}

 

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
function ise 
{
    [CmdletBinding()]
    param (
        [parameter(Mandatory=$True, Position=0, ValueFromPipeline=$True)]
        [psobject]$file,
        [Parameter()]
        [switch] $PassThru = $false 
    )   
    process 
    {
        if ($PassThru)
            { $file }       
        $psise.CurrentOpenedRunspace.OpenedFiles.Add(($file | get-fullpath))
    }
}

I think the PS parser should be smart enough that double-parens are not required in the second example, but oh well.  Pretty simple overall, and super convenient.  Enjoy!

TFS Powershell cmdlets default to -noprompt

Monday, March 16th, 2009

The title kinda says it all.  But as usual, there’s a backstory to tell and some ramifications to warn you about.

First, let’s look back at the legacy tf.exe command line.  In the effort to be both user friendly and flexible, its behavior has actually become quite complicated.  As of TFS 2008, the logic appears to be evaluated in this order of precedence:

  1. If the /prompt flag is present for this command, always use Prompt mode, otherwise
  2. If the /noprompt flag is present for this command, always use Noprompt mode, otherwise
  3. If the tf.exe process’ stdout stream is redirected, use Noprompt mode (unless the environment variable TFS_IGNORESTDOUTREDIRECT is set), otherwise
  4. If executing in script mode with a @command file, use Noprompt mode (unless the most recent invocation of the setnoprompt command was “setnoprompt false”), otherwise
  5. Default to Prompt mode

Yikes.  I’d actually forgotten just how many variables there were until I started running tests for this blog post.  Suffice to say, the Powershell cmdlets approach things in a much simpler way:

  1. If the –prompt switch is present for this cmdlet, always use Prompt mode, otherwise
  2. Default to Noprompt mode

Simple enough.  I know a lot of power users will rejoice at this, having long since resigned themselves to /i muscle memory lest random WinForms interrupt their console workflow.  However, there are ramifications to this behavior that you should be aware of.  “Prompt mode” means more than simply popping up dialogs.  First of all, there are some UI features that are still not accessible in Prompt mode without additional parameters, such as tf diff /configure.  More importantly, leaving Prompt mode turns off a number of safeguards:

  • Undo will permanently discard your local changes.  (I think TFS should use the Recycle Bin, but that’s another show…)
  • Shelve /delete will permanently delete shelvesets.  (The server used to keep them around until a cleanup job ran, but again that’s the way things work now.)
  • Destroy, well, destroys.

These aren’t the only examples; you get the idea.  None of this behavior is new to TFS in general, but with the default settings of the Powershell tool, it’s a lot easier to shoot yourself in the foot.  Even good old Checkin is likely to surprise you the first time you see it simply go without any further interaction.  [a “no blank comments” checkin policy might help here, nudge nudge] 

So have fun, just be careful.  If you’re using the power tools then you are ready to be a power user, right? :)

Why is the TFS Powershell snapin marked 32-bit only?

Friday, March 13th, 2009

Short answer: because the TFS core assemblies are marked 32-bit only.

Like everyone else, I wish the TFS team had supported x64 much earlier on.  You can see in Microsoft.TeamFoundation.Common.dll that while there are a decent number of routines under the NativeMethods static class that need to be ported, it’s not an enormous task.  Turns out that supporting setup and deployment on x64 servers (with all the requisite expansion of the test matrix, etc.) is by far the higher cost.  Good news is that x64 is supposed to be on the Rosario feature list.

In the meantime, I can’t have John Robbins’ impression of our power tool be negative!  I consider my trip to attend his “Native Windows Debugging” class one of the highlights of my MS career.  As a TFS power user I frequently live in x86 shells, so here’s a tidbit from my $profile that helps keep me sane:

$isX86Process = ( $env:Processor_Architecture -eq "x86" ) ... $Global:PscxHostTitlePreference = { $(if ($IsAdmin) { "Admin: " }) + $PscxWindowTitlePrefix + $(get-location).Path + $(if ($IsX86Process) {" (x86)"}) }

Naturally, I like Vivek’s auto-Fork example too – may need to incorporate somewhere into my library of stuff…

How to diff a file that’s not in your workspace?

Wednesday, March 11th, 2009

The documentation for “tf diff” indicates that it should be possible to do things like
tf diff /server:myserver.com c:\file.txt $/project/file.txt

Unfortunately, this syntax is not supported.  (Neither is the /workspace parameter, in case you were wondering.)  It’s an oversight I hope will be corrected in the future.  Until then, here’s a quick fix:

tf view $/project/file.txt > temp.txt
tf diff c:\file.txt temp.txt
del temp.txt

This technique should be easy to put into your scripting language of choice.

More on ConvertTo-FixedByte vs Powershell Community Extensions

Wednesday, February 25th, 2009

Last time we introduced two undocumented cmdlets that shipped with the TFS Fall ‘08 power tools.  The idea behind ConvertTo-FixedByte is probably familiar to anyone who’s used Format-Byte from PSCX.  Take a number and make it pretty.  That’s it -- even the dirt simple conversion from “0” to “<DIR>” is handled in the formatter XML, not the cmdlet.

However, there are some key differences:

  • ConvertTo-FixedByte lets you specify any width between 6 and 22, while Format-Byte is fixed at 10 characters.
  • ConvertTo-FixedByte allows the entire UInt64 range (~10^20), while Format-Byte only goes up to TB (~10^15).
  • ConvertTo-FixedByte does not allow pipeline input, while Format-Byte does.
  • ConvertTo-FixedByte uses the SI standard prefixes, as recommended by IEC, IEEE, EU, and NIST.  Format-Byte uses binary-based prefixes (although it does not display them as KiB, MiB, etc).
    PS> convertto-fixedbyte 2000 -width 10
          2 KB
    PS> format-byte 2000
      1.953 KB
  • ConvertTo-FixedByte uses the biggest possible unit so long as the value is at least one unit.  Format-Byte waits until you’ve exceeded 2^n to use the next biggest unit.
    PS> convertto-fixedbyte 1000 -width 10
          1 KB
    PS> format-byte 1024
       1024 B
  • Both cmdlets attempt to output a fixed width by rounding, but Format-Byte’s approach fails to handle some edge cases correctly:
    PS> convertto-fixedbyte (1000*999) -width 10
        999 KB
    PS> convertto-fixedbyte (1000*999+1) -width 10
    999.001 KB
    PS> convertto-fixedbyte (1000*999+499) -width 10
    999.499 KB
    PS> convertto-fixedbyte (1000*999+500) -width 10
          1 MB
    PS> convertto-fixedbyte (1000*999+501) -width 10
          1 MB
    PS> convertto-fixedbyte (1000*1000-1) -width 10
          1 MB
    PS> convertto-fixedbyte (1000*1000) -width 10
          1 MB
    PS> convertto-fixedbyte (1000*1000+1) -width 10
          1 MB
    PS> format-byte (1024*1023)
       1023 KB
    PS> format-byte (1024*1023+1)
    1023.001 KB
    PS> format-byte (1024*1023+511)
    1023.499 KB
    PS> format-byte (1024*1023+512)
    1023.5 KB
    PS> format-byte (1024*1023+513)
    1023.501 KB
    PS> format-byte (1024*1024-1)
    1023.999 KB
    PS> format-byte (1024*1024)
       1024 KB
    PS> format-byte (1024*1024+1)
          1 MB
  • Both cmdlets omit trailing 0’s, instead padding the front with extra spaces.  [thought there was a slight difference here, but now I can’t repro it!]
  • And of course: PSCX is open-source, while the Power Tools are not.  So I can’t read the PSCX source and Keith can’t read mine.  Even so, I hope some of the ideas can cross-pollinate :)

How does the TFS snap-in handle formatting?

Wednesday, February 25th, 2009

Powershell formatting is pretty flexible, as long as you know how it works.  So how do the version control cmdlets work?  Open up %programfiles(x86)%\Microsoft Team Foundation Server 2008 Power Tools\PowerShell\Microsoft.TeamFoundation.PowerTools.PowerShell.format.ps1xml and have a look for yourself.  Let’s consider the ubiquitous Item class for now:

<View> <Name>Item</Name> <ViewSelectedBy> <TypeName>Microsoft.TeamFoundation.VersionControl.Client.Item</TypeName> </ViewSelectedBy> <TableControl> <TableHeaders> <TableColumnHeader> <Width>7</Width> <Alignment>Right</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>CheckinDate</Label> <Width>10</Width> <Alignment>Right</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>ContentLength</Label> <Width>7</Width> <Alignment>Right</Alignment> </TableColumnHeader> <TableColumnHeader> <Label>ServerItem</Label> <Alignment>Left</Alignment> </TableColumnHeader> </TableHeaders> <TableRowEntries> <TableRowEntry> <TableColumnItems> <TableColumnItem> <PropertyName>ChangesetId</PropertyName> </TableColumnItem> <TableColumnItem> <ScriptBlock>$_.CheckinDate.ToString('d')</ScriptBlock> </TableColumnItem> <TableColumnItem> <ScriptBlock> if ($_.ItemType -eq [Microsoft.TeamFoundation.VersionControl.Client.ItemType]::Folder) { '&lt;DIR&gt;' } else { ConvertTo-FixedByte $_.ContentLength 7 } </ScriptBlock> </TableColumnItem> <TableColumnItem> <ScriptBlock> ConvertTo-FixedPath $_.ServerItem 7,10,6 </ScriptBlock> </TableColumnItem> </TableColumnItems> </TableRowEntry> </TableRowEntries> </TableControl> </View>

And a few examples of what it looks like, taken from an 80- and a 112-character-wide console:

PS> tfdir Changes CheckinDat Content ServerItem etId e Length ------- ---------- ------- ---------- 8224 2/25/2009 <DIR> $/Test-ConchangoV2/super/u...g/directorynamefromhell 8224 2/25/2009 11 KB $/Test-ConchangoV2/super/u...efromhell/devilsurf.jpg 8224 2/25/2009 12 B $/Test-ConchangoV2/super/u...orynamefromhell/foo.txt 8224 2/25/2009 5 MB $/Test-ConchangoV2/super/u.../PowerGUI.1.6.1.639.msi PS> tfdir Changes CheckinDat Content ServerItem etId e Length ------- ---------- ------- ---------- 8224 2/25/2009 <DIR> $/Test-ConchangoV2/super/ultra/very/very/mega/long/directorynamefromhell 8224 2/25/2009 11 KB $/Test-ConchangoV2/super/ultra/very/very/m...ong/directorynamefromhell/devilsurf.jpg 8224 2/25/2009 12 B $/Test-ConchangoV2/super/ultra/very/very/mega/long/directorynamefromhell/foo.txt 8224 2/25/2009 5 MB $/Test-ConchangoV2/super/ultra/very/very/m...torynamefromhell/PowerGUI.1.6.1.639.msi

You can see that the Powershell team’s “expand toward the right to minimize visual variance” concept is used to great effect.  However, we violated their “never provide a label” maxim in 3 out of 4 columns.  Oops!  Seriously though, I don’t think anyone would want to use a ScriptProperty with the exact parameters found in the formatter XML.  What we provided instead is far more useful: a couple of fully generic cmdlets named ConvertTo-FixedByte and ConvertTo-FixedPath.  I’ll cover the former in another post; there’s a fair amount of detail to dig into, but its use here is rather mundane. 

ConvertTo-FixedPath is what really makes this example shine.  If you’re like me, you hate managing long paths in shell environments.  Wrapping onto multiple lines, truncating the front, and (especially) truncating the end of the path are all unacceptable.  Cmdlets to the rescue!  This one has two modes (“parameter sets” in PS-speak).  The first is obvious enough:

PS> convertto-fixedpath 12345678901234567890 -width 16

12345678...67890

The 20-character input string is truncated to 16, but only by omitting things in the middle.  (Note that this mode has an off-by-one bug.  Hint: odd numbers.)  This allows you to answer the important questions: what is the file name? what branch/workspace are they located in?  Usually we don’t care as much about the intermediate directories.  Maybe the same is true for other kinds of strings too – let me know if you find it useful!

The other mode is designed explicitly for use in a ps1xml formatter.  Consider:

PS> convertto-fixedpath 12345678901234567890 -otherwidths 30, 20, 10

12345678...67890

Same output, but what the heck are the “other widths”?  Imagine this string were going into a variable-length field.  We want the string to expand dynamically, showing us as much of the filename as possible.  Yet if it does need to be cropped, we want it done intelligently, as if we’d known the width in advance and passed it as a –width parameter.

As it turns out, we can know the final width, given a little hackery.  The Powershell API lets you access the raw console host details, including its BufferSize.  All you need to do is subtract the widths of the other columns, plus room for the separator character PS places between each column.  So imagine the code above is used in a four-column formatter: 80 - 30 - 20 - 10 - (4*1) = 16.  Sure enough, if you scroll back up to the XML, you’ll see the values passed to –otherwidths are no coincidence.  We hand-tweaked every column width so it fit the data type as closely as possible without wrapping*, then plugged in those widths to the convertto-fixedpath cmdlet driving the final variable-length column.

This hack may not always work as Powershell migrates away from the ancient CSRSS hosting environment.  But in theory, anyone who implements PSHostRawUserInterface should be compatible.  It works in the Powershell v2 ISE environment that ships with Win7, for instance.  Custom-PS-tool-lovers, let me know how it goes.

*Except for column titles, obviously.  They were a necessary casualty of two higher-priority design considerations (a) keeping labels identical to the underlying property name (b) supporting the default 80-character console width without squishing average-sized paths.

What does Get-TfsServer –All do, exactly?

Tuesday, February 17th, 2009

PS C:\workspaces\ws1> help tfserver –full

SYNTAX
    Get-TfsServer [-Name] <String> [-Credential <PSCredential>] [-All] [<Common
    Parameters>] 
  

    Get-TfsServer -Path <String> [-Credential <PSCredential>] [-All] [<CommonPa
    rameters>]

...

  -All
    Get Team Foundation Server object with the extended properties

  Required?                    false
    Position?                    named
    Default value
    Accept pipeline input?       false
    Accept wildcard characters?  false

... 

Not the greatest help text ever written, I know.  Oh well, there’s a lot more room on this blog anyway :)

First the basics: Get-TfsServer and Get-TfsWorkspace (aliases: tfserver, tfworkspace) are intended to be “official” versions of James Manning’s get-tfs.ps1 and get-workspace.ps1 cmdlets.  To incorporate them into the main snap-in, Hyung ported them to C# and standardized the parameters.  Both will now accept either –Path or –Name (but not both).  For tfserver, –Name is the default [position = 1] and all others are named, while the reverse is true for tfworkspace, reflecting what we felt was the most common use of each.  Bottom line, these will all work:

> $tfs = get-tfsserver njtfs
> $tfs = get-tfsserver –path .
> $ws = get-tfsworkspace .
> $ws = get-tfsworkspace –name njtfs
> $ws = get-tfsworkspace –servername .

There are also new parameters like –Credential on tfserver, and –Computer and –Owner on tfworkspace, which behave more or less how you’d expect.  In short, you can use tfserver to interface TFS with Powershell’s built-in credential management features, as well as tfworkspace to query for the Workspace object that represents remote workspaces you may not even own.

So back to the original topic.  The –All switch turns on the special script-friendly behavior that James pioneered and I later extended.

AddTypeExtensions

As you can see there are a few differences between the final shipping behavior and the various get-tfs.ps1 scripts floating on the web:

  • Once you choose –All, every TFS assembly is loaded immediately, rather than on-demand as you use the ScriptProperties.  The idea here is to “fail early” rather than introduce a ticking bomb into downstream scripts that may depend on these objects.
  • The types that we import as NoteProperties are prefixed with the same shorthand as the ScriptProperties, plus an underscore.  Even though we continue to weed out types that aren’t useful (anything private, an exception, or an event) there are still a ton to sort thru; this tweak helps segregate the huge list into “namespaces” that are still marginally navigable with Tab completion.
  • C# is a much uglier language to do this work than Powershell!

All in all, you can now do cool things like:

PS C:\workspaces\ws1> $tfs = get-tfsserver njtfs -all
PS C:\workspaces\ws1> $tfs.vcs.GetChangeset(1234) 
 

Changes Owner                  CreationDa Comment
   etId                                te
------- -----                  ---------- -------
   1234 COATUECAP\LDeGrazia    10/27/2007

PS C:\workspaces\ws1> new-object $tfs.VCS_ChangesetVersionSpec 1234

                            ChangesetId DisplayString
                            ----------- -------------
                                   1234 C1234

So why not have this option switched on permanently?  The vanilla TeamFoundationServer object isn’t very useful on its own; the entire point of James’ original wrapper was to quickly access the VersionControlServer and similar objects.  Well first of all, it’s noticeably slower – on my machine, testing a “cold” Powershell console, the cmdlet takes 3 seconds with the default parameters and 6 seconds with –All.  We’re loading more assemblies than ever, not to mention deep reflection on each one (in order to test an upcast and a string comparison, not exactly speedy operations in themselves).  More importantly, the extra properties add enormous clutter to the objects when not needed.  And I think you’ll be more likely to use those TFS objects in vanilla form in your future scripts.  For example, most of the version control cmdlets in the Fall 2008 suite take a TeamFoundationServer as either pipeline input or a named parameter.  I’m sure whatever Microsoft is cooking up for the next batch will include even more interactivity and flow between cmdlets.

Unlocking other people’s files. Yes, again.

Saturday, February 14th, 2009

You’re going about your business, trying to refactor some code, when suddenly things come to a crashing halt.  “The item foo.cs is locked for check-out by DOMAIN\user in workspace ws1.”  Since Microsoft shipped the first public betas of TFS in 2005, there have probably been more blog posts on this pitfall than any other -- I’ll pick on James for an example.  The frustration also spawned Status Sidekick (now part of TFS Sidekicks), the first 3rd-party TFS tool to my knowledge. 

The arrival of the Fall 2008 Powertools hinted at yet another possibility based on Powershell.  Script-friendly unlocking is particularly enticing for scenarios like the one I found myself in last week: transitioning a team from exclusive checkouts to an edit-merge-commit workflow.  Turning off the exclusive settings on the Team Project is the obvious first step, but what to do about the hundreds of existing pending changes?  I can’t ask everyone to suddenly checkin their work (which may be far from ready to share, or even build), but I also can’t reorganize the tree unless every lock is removed.  Thus:

del temp.hat 2> $null foreach ($pendingSet in get-tfspendingchange -user *) { $workspace = '"' + $pendingSet.Name + '";' + $pendingSet.OwnerName $pendingSet.PendingChanges | where {$_.IsLock} | foreach { $item = '"' + $_.ServerItem + '"' "lock /lock:none /workspace:$workspace $item" | out-file temp.hat -append } } tf "@temp.hat" del temp.hat

This isn’t the cleanest solution in the world.  You couldn’t just wire up the “find all locks in the system” example I provided for Brian’s post to a Remove-TfsLock cmdlet -- even if the Power Tools provided one (which they don’t yet).  By convention, a pipeline of TFS cmdlets transmits QualifiedItems, but to unlock an arbitrary item we need two additional pieces of information from the PendingSet object.  So we fall back on good old procedural loops around tf lock.

Could we improve the script?  Sure.  We could batch ~12 items at a time* for a better performance, though the gain is not nearly as big as using a @hatfile in the first place.  We could also be a little more robust.  Get-TfsPendingChange returns a full PendingSet in the general case, but sometimes the return type decays to a simple PendingChange[].  If our query returns exactly 1 workspace, then our logic for populating $workspace won’t work. 

Of course, we could also do a lot worse!  I originally tried to add the wrapper quotes in-place, but quickly got into syntax trouble.  After consulting the relevant Powershell blog post I eventually got it right, but it was an unreadable mess.  For example, line 4 read:

$workspace = "`"$($pendingSet.Name)`";$($pendingSet.OwnerName)"

Yuck.

*3200 character limit on each line of a tf.exe script file; TFS paths are 260 chars max

    Get-TfsPendingChange doesn’t always return pending changes

    Saturday, February 14th, 2009

    Depending on how extensively you’ve played with the new TFS powershell cmdlets, you may have noticed one oddity: Get-TfsPendingChange doesn’t always return a PendingChange[] array.

    PS C:\workspaces\ws1\MAIN> tfstatus

    Version CreationDa ChangeType      ServerItem
                    te
    ------- ---------- ----------      ----------
       7415  2/13/2009 Edit            $/Research Platform (R...rLife.v2 Web.vssscc
       8024  2/13/2009 Edit            $/Research Platform (R...ckerLife.v2 Web.sln

    PS C:\workspaces\ws1\MAIN> tfstatus -user rwilliams

    Name                Computer            OwnerName           Type
    ----                --------            ---------           ----
    CoatueResearch      CMXP123             COATUECAP\rwilliams Workspace
    BuildTypeEditor_... CMXP123             COATUECAP\rwilliams Workspace

    The second command is actually returning a PendingSet[] array.  Each PendingSet contains a PendingChange[] array that’s specific to a particular workspace or shelveset, plus some metadata about the workspace or shelveset itself.

    What gives?  Actually, the underlying APIs always return PendingSet[].  If Get-TfsPendingChange merely pushed the result of the API call onto the pipeline, as most other TFS cmdlets do, then every time you ran it you’d get something more like this:

    PS C:\workspaces\ws1\MAIN> $ws = get-tfsworkspace .
    PS C:\workspaces\ws1\MAIN> $ws.QueryPendingSets($null, $ws.Name, $ws.OwnerName, $false)

    Name                Computer            OwnerName           Type
    ----                --------            ---------           ----
    ws1                 RICHARD490          COATUECAP\rberg     Workspace

    …which is clearly not as useful as the output of the first “tfstatus” example above.  Furthermore, people only use Get-TfsPendingChange (and its tf.exe predecessor) to query across multiple workspaces 5% of the time, if that.  I decided to optimize for the 95% case.  That meant checking to see how many PendingSets were returned from the server, pushing its PendingChange[] array onto the pipeline instead if there was only 1.

    Note: the QueryShelvedChanges API we use takes a single shelveset.  Unfortunately that means the cmdlet does not support queries like “show me everyone who has shelved $/project/foo.cs” in a single server call.  (QueryShelvedChanges() does have the necessary overloads; we simply inherited this limitation from tf.exe and didn’t have time to rewrite it).  Thus, whenever you specify the –shelveset parameter to Get-TfsPendingChange, you are assured to get a PendingChange[].