get a multi-version diff report from TFS? - powershell

I used to use a different source control tool and it allowed me to get a "diff report": all the changes made to a file between version X and version Y (including lines added/removed between each version, which could be many versions) in one text file. It was pretty handy for situations where you are pretty sure that some code used to be in your file but now it's not (handy for when your BA says to add something and you're thinking "didn't I take that out?!").
The advantage here is that you get one text file that has all the changes to a codebase that you can then search. This is equivalent to doing a compare on every version (10 to 9, 9 to 8 etc) and then saving the results of every compare to a text file.
I don't see any easy way to do this in TFS. Is there a plugin/powertool that does this? The google gave me nothing.

I'm not aware of any out-of-the-box solution. However, it isn't hard to make one yourself if you have TFS Power Toys and PowerShell. Try this in PowerShell:
Add-PSSnapin Microsoft.TeamFoundation.PowerShell
Get-TfsItemHistory foo.cs | foreach {
tf diff "foo.cs;C$($_.ChangesetId)" `
"foo.cs;C$($_.ChangesetId - 1)" `
/format:unified
}

Pavel got me going in the right direction, but the script I ended up with was a lot more complex. And still might not be correct. I had to account for filename changes.
$snapin = get-pssnapin | select-string "Microsoft.TeamFoundation.PowerShell"
if ($snapin -eq $null) {
Write-Host "loading snap in..."
Add-PSSnapin Microsoft.TeamFoundation.PowerShell
}
$fileName = $args[0] Write-Host "// File name " $fileName
$results = #(Get-TfsItemHistory $fileName ) | select changesetid, #{name="Path"; expression={$_.changes[0].item.serveritem}}
$i = 0
$cmdArray = #()
do {
if ( $results[$i+1] -ne "" ) {
$cmdArray += "tf diff ""{0};{1}"" ""{2};{3}"" /format:unified" -f $results[$i].Path, $results[$i].ChangeSetId, $results[$i+1].Path, $results[$i+1].ChangeSetId
} ;
$i++
} until ($i -ge ($results.length - 1))
foreach ($cmd in $cmdArray) {
#Write-Host "// " $cmd
iex $cmd }

Related

Cleaning up filenames from MAC using Powershell

I'm writing a script that iterates over files that are copied from a MAC computer to a Exfat disk and checks the name of the files for Windows forbidden characters.(Writing it in PowerShell)
And hopefully replace the forbidden characters with another character, for example a "-".
Why i am doing this is because i see it as a good way for me to practice coding and it might be used in my work when we get users with a lot of local files that we want to move to Onedrive.(Onedrive has a function to rename but it doesn't touch the forbidden characters, and i don't know Bash)
The issue is when I'm trying to find the characters within the script itself, it cant find the characters if i write them in the script.(For example if i write that it should look for ">")
Even if i escape the characters it just skips it(Or rather doesn't find it).
It just skips over the file i know has one in it, at first i though it might be due to encoding, but no matter what i use as a default encoding it wont display the filename correctly.(I assume this is due to how Windows reads filenames?)
edit: These are the forbidden characters im goint to look for " * : < > ? / \ |
The script itself is able to remove letters and stuff if i ask it to.
I also tried getting the char of the byte([byte][char]"") but i get this error:
Cannot convert value "" to type "System.Byte". Error: "Value was either too large or too small for an unsigned byte." Edit: the  changed by itself during the day not sure what to say.
If i just add it to the function it just returns the error:Rename-Item : The input to the script block for parameter 'NewName' failed. Exception calling "Replace" with "2" argument(s): "String cannot be of zero length.
The characters are displayed like this in visual studio code.
Any ideas are welcome or if you know of any better ways of doing it?(Maybe its better if i just learn bash. )
Here is the script itself:
edit: cleaned up the script abit and some small changes.
$provided_path = Read-Host "What Directory and its subfolders do you want to check?"
# Iterates over the folders and files within.
Write-Host "Creating list of files..."
$dictionary_filenames = ""
$dictionary_filenames = Get-ChildItem -Path $provided_path -Recurse -Force -File | Select-Object FullName,BaseName,Extension
# Resetting counter
$counter_skipped = 0
# Function for character replacment
function rename_file_name($names_function,$forbidden_char){
$old_name = $names_function.BaseName
$new_name = $names_function | Rename-Item -LiteralPath $names_function.FullName -NewName{$_.BaseName.Replace("$forbidden_char","-") + $_.extension} -PassThru
if($old_name -ne $new_name.BaseName){
Write-host "$old_name changed to $new_name"
}
}
foreach($names in $dictionary_filenames){
if($names.BaseName[0] -eq "." -and $names.BaseName[1] -match "_"){
$counter_skipped ++
$counter ++
continue
}
else{
if($names.BaseName[0] -eq " " -or $names.BaseName[-1] -eq " "){
$old_name = $names.BaseName
$new_name = $names | Rename-Item -LiteralPath $names.FullName -NewName{$_.BaseName.trim() + $_.extension} -PassThru
Write-Host "Trimming whitespace: $old_name"
}
else{
Write-Host "Trimming not needed "$names.BaseName
}
Write-host "Checking forbidden characters "$names.BaseName
rename_file_name $names ">"
rename_file_name $names "<"
rename_file_name $names "/"
}
}
Write-Host "Files checked: "$dictionary_filenames.Count
Write-Host "Files Skipped: "$counter_skipped
Read-Host 'Close window by pressing "Enter"'```
 is U+F021, two bytes long or [int16]. You can put it in a utf8 with bom encoded script.

Why order is not maintained while reading a text file from Powershell?

Below is my Powershell script:
$server_file = 'serverlist.txt'
$servers = #{}
Get-Content $server_file | foreach-object -process {$current = $_.split(":"); $servers.add($current[0].trim(), $current[1].trim())}
foreach($server in $servers.keys){
write-host "Deploying $service on $server..." -foregroundcolor green
}
My serverlist.txt looks like this:
DRAKE : x64
SDT: x64
IMPERIUS : x64
Vwebservice2012 : x64
Every time I run this script, I get IMPERIUS as my server name. I would like loop through the servers in the order they are written in serverlist.txt.
Am I missing anything in Get-Content call?
Don't store servers in a temporary variable.
The iteration order of hashtables (#{}) is not guaranteed by the .NET framework. Avoid using them if you want to maintain input order.
Simply do:
$server_file = 'serverlist.txt'
Get-Content $server_file | ForEach-Object {
$current = $_.split(":")
$server = $current[0].trim()
$architecture = $current[1].trim()
Write-Host "Deploying $service on $server..." -ForegroundColor Green
}
Note: Even if it probably won't make much of a difference in this particular case, in general you always should explicitly define the file encoding when you use Get-Content to avoid garbled data. Get-Content does not have sophisticated auto-detection for file encodings, and the default it uses can always be wrong for your input file.

net files /close does not work as expected

We have many self-written applications which are executed in a network location. Whenever a developer wants to publish a new version of this application, I have to close all opened files on the server where the app is located, by going to:
computer management > Shared Folders > Opened Files
then choose all the files of the application - right click and close.
I want the developers to do that by themselves via PowerShell, so I wrote a Unlock-Files function. This part of the function should close the files, with the net files $id /close call:
$result = Invoke-Command -ComputerName $server -Credential $cred -ArgumentList $workpath, $server {
param($workpath,$server)
$list = New-Object System.Collections.ArrayList
$ErrorActionPreference = "SilentlyContinue"
$adsi = [adsi]"WinNT://./LanmanServer"
$resources = $adsi.psbase.Invoke("resources") | % {
[PSCustomObject] #{
ID = $_.gettype().invokeMember("Name","GetProperty",$null,$_,$null)
Path = $_.gettype().invokeMember("Path","GetProperty",$null,$_,$null)
OpenedBy = $_.gettype().invokeMember("User","GetProperty",$null,$_,$null)
LockCount = $_.gettype().invokeMember("LockCount","GetProperty",$null,$_,$null)
}
}
$resources | ? { $_.Path -like $workpath } | tee -Variable Count | % {
$id = $_.ID
net files $id /close > $null
if ($LASTEXITCODE -eq 0) { $list.add("File successfully unlocked $($_.path)") > $null }
else { $list.add("File not unlocked (maybe still open somewhere): $($_.path)") > $null }
}
if (!( ($count.count) -ge 1 )) { $list.add("No Files to unlock found in $workpath on $server") > $null }
$list
}
I expect net files /close to behave the same way as the manual way I do directly on the server described above, but it doesn't behave like this at all. It sometimes closes the file, sometimes not. But it never closes all the necessary files, so the developer can not publish his application. also, net files almost never finishes with LastExitCode 0, but I can't get my head around why.
Is there a way to force net files to really close all the files?
why would net files behave different than closing the files manually?
Is there a native PowerShell way to do this?
Is there a list of last exit codes, so I can wrap my head around this issue further?
Thank you!

PowerShell - Sorry, we couldn't find Microsoft.PowerShell.Core\FileSystem::

I'm trying to modify the script created by Boe Prox that combines multiple CSV files to one Excel workbook to run on a network share.
When I run it locally, the script executes great and combines multiple .csv files into one Excel workbook.
Clear-Host
$OutputFile = "ePortalMonthlyReport.xlsx"
$ChildDir = "C:\MonthlyReport\*.csv"
cd "C:\MonthlyReport\"
echo "Combining .csv files into Excel workbook"
. C:\PowerShell\ConvertCSVtoExcel.ps1
Get-ChildItem $ChildDir | ConvertCSVtoExcel -output $OutputFile
echo " "
But when I modify it to run from a network share with the following changes:
Clear-Host
# Variables
$OutputFile = "ePortalMonthlyReport.xlsx"
$NetworkDir = "\\sqltest2\dev_ePortal\Monthly_Report"
$ChildDir = "\\sqltest2\dev_ePortal\Monthly_Report\*.csv"
cd "\\sqltest2\dev_ePortal\Monthly_Report"
echo "Combining .csv files into Excel workbook"
. $NetworkDir\ConvertCSVtoExcel.ps1
Get-ChildItem $ChildDir | ConvertCSVtoExcel -output $OutputFile
echo " "
I am getting an error where it looks like it using the network path twice and I am not sure why:
Combining .csv files into Excel workbook
Converting \sqltest2\dev_ePortal\Monthly_Report\001_StatsByCounty.csv
naming worksheet 001_StatsByCounty
--done
opening csv Microsoft.PowerShell.Core\FileSystem::\sqltest2\dev_ePortal\Monthly_Report\\sqltest2\dev_ePortal\Monthly_Report\001_StatsByCounty.csv) in excel in temp workbook
Sorry, we couldn't find Microsoft.PowerShell.Core\FileSystem::\sqltest2\dev_ePortal\Monthly_Report\\sqltest2\dev_ePortal\Monthly_Report\001_StatsByCounty.csv. Is it possible it was moved, renamed or deleted?
Anyone have any thoughts on resolving this issue?
Thanks,
Because in the script it uses the following regex:
[regex]$regex = "^\w\:\\"
which matches a path beginning with a driveletter, e.g. c:\data\file.csv will match and data\file.csv will not. It uses this because (apparently) Excel needs a complete path, so if the file path does not match, it will add the current directory to the front of it:
#Open the CSV file in Excel, must be converted into complete path if no already done
If ($regex.ismatch($input)) {
$tempcsv = $excel.Workbooks.Open($input)
}
ElseIf ($regex.ismatch("$($input.fullname)")) {
$tempcsv = $excel.Workbooks.Open("$($input.fullname)")
}
Else {
$tempcsv = $excel.Workbooks.Open("$($pwd)\$input")
}
Your file paths will be \\server\share\data\file.csv and it doesn't see a drive letter, so it hits the last option and jams $pwd - an automatic variable of the current working directory - onto the beginning of the file path.
You might get away if you edit his script and change the regex to:
[regex]$regex = "^\w\:\\|^\\\\"
which will match a path beginning with \\ as OK to use without changing it, as well.
Or maybe edit the last option (~ line 111) to say ...Open("$($input.fullname)") as well, like the second option does.
Much of the issues are caused in almost every instance where the script calls $pwd rather than $PSScriptRoot. Replace all instances with a quick find and replace.
$pwd looks like:
PS Microsoft.PowerShell.Core\FileSystem::\\foo\bar
$PSScriptRoot looks like:
\\foo\bar
The second part i fixed for myself is what #TessellatingHeckler pointed out. I took a longer approach.
It's not the most efficient way...but to me it is clear.
[regex]$regex = "^\w\:\\"
[regex]$regex2 = "^\\\\"
$test = 0
If ($regex.ismatch($input) -and $test -eq 0 ) {
$tempcsv = $excel.Workbooks.Open($input)
$test = 1 }
If ($regex.ismatch("$($input.fullname)") -and $test -eq 0) {
$tempcsv = $excel.Workbooks.Open("$($input.fullname)")
$test = 1}
If ($regex2.ismatch($input) -and $test -eq 0) {
$tempcsv = $excel.Workbooks.Open($input)
$test = 1 }
If ($regex2.ismatch("$($input.fullname)") -and $test -eq 0) {
$tempcsv = $excel.Workbooks.Open("$($input.fullname)")
$test = 1}
If ($test -eq 0) {
$tempcsv = $excel.Workbooks.Open("$($PSScriptRoot)\$input")
$test = 0 }

Powershell Script: prompt a file (by mask) and use that file in a command line

Disclaimer: I don't know enough about ps to accomplish this in a reasonable amount of time, so yes, I am asking someone else to do my dirty job.
I want to be able to run a web.config transformation without opening a command line.
I have following files in a folder:
web.config - actual web config
web.qa.config - web config transformation for qa env
web.production.config - web config transformation for production env
transform.ps1 - powershell script I want to use to run transformation
Here is what I want:
PS file shall enumerate current directory using .*\.(?<env>.*?)\.config and let me choose which <env> I am interested in generate web.config for. In my example I will be presented with two options: "qa", "production".
After I (user) select the environment (let's say it is "qa", selected environment is stored as $env, and corresponding filename will be stored as $transformation) script shall do following:
backup original web.config as web.config.bak
execute following command:
.
echo applying $transformation...
[ctt][1].exe source:web.config transformation:$transformation destination:web.config preservewhitespaces verbose
echo done.
ctt.exe is a tool based on XDT that runs web.config transformation from command line.
Okay, looks simple enough, I'll do your dirty job for you. ;)
Save the following as transform.ps1:
$environments = #()f
gci | %{if ($_ -match '.*\.(?<env>.*?)\.config') {$environments += $matches.env}}
Write-Host "`nEnvironments:"
for ($i = 0; $i -lt $environments.Length; $i++) {Write-Host "[$($i + 1)] $($environments[$i])"}
Write-Host
do {
$selection = [int](Read-Host "Please select an environment")
if ($selection -gt 0 -and $selection -le $environments.Length) {
$continue = $true
} else {
Write-Host "Invalid selection. Please enter the number of the environment you would like to select from the list."
}
} until ($continue)
$transformation = "web.$($environments[$selection - 1]).config"
if (Test-Path .\web.config) {
cpi .\web.config .\web.config.bak
} else {
Write-Warning "web.config does not exist. No backup will be created."
}
if ($?) {
Write-Host "`nApplying $transformation..."
[ctt][1].exe source:web.config transformation:$transformation destination:web.config preservewhitespaces verbose
Write-Host "Done.`n"
} else {
Write-Error "Failed to create a backup of web.config. Transformation aborted."
}