Archive for the ‘TFS’ 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.

How to enforce a rule on Area/Iteration Path

Tuesday, February 17th, 2009

Another pitfall I encountered while setting up our new team project was how to make sure people didn’t cop out and leave them at the default (root, aka useless) value.  With Areas I may have been able to set up some weird non-inheriting permission scheme, but I needed to solve this problem for Iterations anyway, so I turned to the web. 

Unfortunately, the standard answer from the product team didn’t work for me. 

<FIELD type="Integer" name="AreaID" refname="System.AreaId"> <HELPTEXT>AreaID</HELPTEXT> </FIELD> <FIELD type="Integer" name="IterationID" refname="System.IterationId"> <HELPTEXT>IterationID</HELPTEXT> </FIELD> <FIELD type="String" name="Product Area - Validation" refname="Coatue.ProductAreaValidation"> <HELPTEXT>Hidden field used to validate Product Area</HELPTEXT> <WHEN field="System.AreaId" value="84"> <COPY from="value" value="Restricted" /> </WHEN> <PROHIBITEDVALUES> <LISTITEM value="Restricted" /> </PROHIBITEDVALUES> </FIELD> <FIELD type="String" name="Sprint or Release - Validation" refname="Coatue.SprintPathValidation"> <HELPTEXT>Hidden field used to validate Sprint or Release</HELPTEXT> <WHEN field="System.IterationId" value="84"> <COPY from="value" value="Restricted" /> </WHEN> <PROHIBITEDVALUES> <LISTITEM value="Restricted" /> </PROHIBITEDVALUES> </FIELD>

It successfully blocked work items that had Area/Iteration paths I didn’t want, but I’d continue to get validation errors after I changed the paths in the form to something else.

At this point I started to wonder whether the AreaId and AreaPath fields were as intricately tied as I thought they were.  After all, I didn’t have any rules saying to update one when the other changed.  (The documentation kinda implies this but doesn’t spell out the mechanism.)  So I decided to test at the object model level: Get-TfsServer OM wrapper to the rescue!

PS C:\Users\rberg> $tfs = get-tfsserver njtfs –all
PS C:\Users\rberg> $bug = $tfs.wit.GetWorkItem(1333)
PS C:\Users\rberg> $bug.AreaId; $bug.IterationId
84
84  

 

PS C:\workspaces\ws1> $bug.AreaPath; $bug.IterationPath
Test-ConchangoV2\test
Test-ConchangoV2\Release 1\Sprint 4

PS C:\Users\rberg> $bug.fields | where { $_.name.contains("Validation") } | select value
Value
-----
Restricted Restricted                                                                                                                       

PS C:\Users\rberg> $bug.AreaId = 104; $bug.IterationId = 91
PS C:\workspaces\ws1> $bug.AreaPath; $bug.IterationPath
Test-ConchangoV2\test
Test-ConchangoV2\Release 1\Sprint 4 

PS C:\Users\rberg> $bug.fields | where { $_.name.contains("Validation") } | select value
Value
-----
Restricted
Restricted

Nope, not a synchronization issue between path on the form <–> underlying ID.  Looks like I really do need to tweak Gregg’s advice.  All you need is a corresponding WHENNOT rule to cancel out each WHEN rule as it’s no longer needed.  Here’s what the hidden validator for Area looks like now:

<FIELD type="String" name="Product Area - Validation" refname="Coatue.ProductAreaValidation"> <HELPTEXT>Hidden field used to validate Product Area</HELPTEXT> <WHEN field="System.AreaId" value="84"> <COPY from="value" value="Restricted" /> </WHEN> <WHENNOT field="System.AreaId" value="84"> <COPY from="value" value="Ok" /> </WHENNOT> <PROHIBITEDVALUES> <LISTITEM value="Restricted" /> </PROHIBITEDVALUES> </FIELD>

Make the same change for Iteration and you’re golden.

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.

How to prevent people from opening new bugs

Tuesday, February 17th, 2009

We’re in the middle of transitioning to a new team project.  On the source control side, Microsoft has made a huge investment to support arbitrary renames throughout the tree.  No reason to mess with TFS->TFS migration when I can move the native items!  On the other hand, migrating work items across team projects is notoriously tricky.  I decided it would be easiest to steadily phase out the work items remaining in the old project while forcing people to open new issues in the new project.

Being relatively new to WIT customization, the answer wasn’t immediately obvious.  TFS doesn’t let you stick an ALLOWEDVALUES rule on fields like System.CreatedDate that the system populates on creation.  And anything you stick on a user-editable field will inevitably block users from navigating an existing work item through the rest of its legitimate workflow.  The path I took was modifying the initial transition from “”.  XML excerpt:

<TRANSITION from="" to="Active"> <REASONS> <REASON value="Build Failure" /> <DEFAULTREASON value="New" /> </REASONS> <FIELDS> <FIELD refname="Microsoft.VSTS.Common.ActivatedBy"> <COPY from="currentuser" /> <VALIDUSER /> <REQUIRED /> </FIELD> <FIELD refname="Microsoft.VSTS.Common.ActivatedDate"> <SERVERDEFAULT from="clock" /> </FIELD> <FIELD refname="System.AssignedTo"> <DEFAULT from="currentuser" /> </FIELD> <FIELD refname="System.Title"> <ALLOWEDVALUES> <LISTITEM value="Before 2009-02-12" /> </ALLOWEDVALUES> </FIELD> </FIELDS> </TRANSITION>

Most of this comes from the stock MSF Agile v4.2 template; the System.Title field with the strange rule toward the end is my addition.  Doesn’t really matter what you put in the value, so long as you don’t tell your users what it is :)