Visual Studio Add-In for TFS: Marking Files for Deletion - c#

I've been writing a VB Add-in for my company that will go into TFS and automatically mark files that end with ".delete" for deletion. To do this, I'd like to create a workspace "Temp" which maps to my D:\TFSTemp in my local and a folder in TFS. Then, I'd like to download only the .delete files to my local (to avoid having to get latest on all the files in the server), map them from my local to the server, mark them for deletion (workspace.PendDelete()) and then check them all in at once.
My problem is I am not sure I am setting up the correct mapping needed. I am able to download all the .delete files, yet when I invoke Workspace.GetPendingChanges(), the array is not being populated, which is why I suspect I might be not setting it up correctly.
I understand it is a complicated add-in, so please ask me questions if my code does not make sense to you.
//establish connection to tfs
TeamFoundationServer server = new TeamFoundationServer(TFS1);
//test file to output to
StreamWriter xw = new StreamWriter(#"C:\Documents and Settings\A087649\Desktop\FileList.txt");
//get a working object in tfs
VersionControlServer sourceControl = server.GetService(typeof(VersionControlServer)) as VersionControlServer;
int numberOfFiles = 0;
int numToDelete = 0;
try
{
//load config file
//LoadConfig();
//path where we are going to look in tfs
String path = #"$/PAIT_ECOMPARE/Dev/TFSTool/Prod/Offeringdata/AU/CT";
//array of item objects in that path
ItemSet items = sourceControl.GetItems(path, RecursionType.Full);
numberOfFiles = items.Items.Length;
Workspace workspace = sourceControl.CreateWorkspace("Temp");
WorkingFolder workingFolder = new WorkingFolder(path, #"D:\TFSTemp\");
workspace.CreateMapping(workingFolder);
//instance of own created class that represents the progressbar and log output
TFSToolLoad ProgressBar = new TFSToolLoad();
ProgressBar.SetValues(numberOfFiles);
ProgressBar.TopMost = true;
foreach (Item item in items.Items)
{
ProgressBar.Show();
//get only the file path to the file
serverPath = item.ServerItem;
//get changeset Id
changeSetID = item.ChangesetId;
if (serverPath.EndsWith(".delete"))
{
//get file name only and local path
fileName = Path.GetFileName(serverPath);
localPath = #"D:\TFSTemp\"+ fileName;
//get latest on the file
workspace.Get(new GetRequest(serverPath, RecursionType.None, VersionSpec.Latest), GetOptions.None);
workspace.PendDelete(serverPath, RecursionType.None);
numToDelete++;
}
ProgressBar.Step();
}
ProgressBar.SetText
("Number of Files Marked for Delete: " + numToDelete+"\n");
//if there are any pending changes, check them in and merge them into staging
if (numToDelete > 0)
{
//check in all the changes
ProgressBar.SetText("Checking in changes...\n");
PendingChange[] pendingChanges = workspace.GetPendingChanges();
//if there are any pending changes, check them in and merge them into staging
workspace.CheckIn(pendingChanges, "Automated TFS tool cleanup");
ProgressBar.SetText("Done\n Merging changes into Staging...");
//merge
//set up merge by changeset id
ChangesetVersionSpec changeSet = new ChangesetVersionSpec(changeSetID);
//map to target server path before merging, otherwise it won't work
Workspace eCompareAdmin = sourceControl.GetWorkspace(#"D:\PAIT_ECOMPARE");
string mainPath = #"$/PAIT_ECOMPARE/Dev/TFSTool";
string stagingPath = #"$/PAIT_ECOMPARE/Dev/TFSToolStaging";
//Problem:
eCompareAdmin.Merge(mainPath, stagingPath, changeSet, changeSet);
PendingChange[] mergeChanges = eCompareAdmin.GetPendingChanges();
workspace.CheckIn(mergeChanges, "Automated TFS Cleanup");
ProgressBar.SetText("Done\n");

You can't pend a delete until you've done a get of the item into your local workspace.
You're currently doing a DownloadFile, which will simply get the contents of the file - it will not update your workspace to reflect that you have the file locally. You should instead call Workspace.Get for that file before pending your delete.
One other item: you should not be using full recursion for files (without being aware of the consequences), you should probably be using RecursionType.None. Full recursion on a file performs pattern matching and will include all files with that filename beneath the given path. (Ie, full recursion for $/file.txt will match $/file.txt, $/A/file.txt, $/A/B/file.txt, etc.)

Related

TFS - Get latest code in a folder

I am using the TFS API to get latest code files, directories, .csproj files, etc. under a TFS-bound folder.
For the same, I use something like the following:
var tfs = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(new
Uri(ConfigurationManager.AppSettings["TFSUrl"]));
tfs.EnsureAuthenticated();
var vsStore = tfs.GetService<VersionControlServer>();
string workingFolder = #"C:\TFS\SolutionFolder";
Workspace wsp = vsStore.TryGetWorkspace(workingFolder);
if (wsp != null)
{
ItemSet items = vsStore.GetItems(workingFolder, VersionSpec.Latest, RecursionType.Full);
string relativePath = workingFolder + #"/";
foreach (Item item in items.Items)
{
string relativePath1 = item.ServerItem.Replace("$/TFS/SolutionFolder", relativePath);
if (item.ItemType == ItemType.Folder)
{
Directory.CreateDirectory(relativePath1);
}
else
{
item.DownloadFile(relativePath1);
}
}
}
Now, I get the items to download and then download happens. However, I want it to be like how VS handles it - if (and only if) there is a change in a file/folder, then only download the same. With this code, I always get 'n' number of files/folders in that folder and then I overwrite the same. Wrong approach, I know. I can, however, modify this code to check for the folder's or file's last change time and then choose to either overwrite it or ignore it. That's an option, albeit a bad one at that.
Now, what I would ideally like is to get ONLY the list of files/folders that actually need to be changed i.e. the incremental change. After that, I can choose to overwrite/ignore each item in that list. So, in the present case, if a new file/folder is created (or one of the existing ones got changed inside $/TFS/SolutionFolder i.e. in the sever), then only I want to pull that item in the list of files/folders to change(and decide what I want to do with it inside C:\TFS\SolutionFolder).
Also, is using one of the overloads of VersionControlServer.QueryHistory() an option? I had something like this:
(latestVersionIdOf $/TFS/SolutionFolder) - (existingVersionIdOf C:\TFS\SolutionFolder) = (Versions that I'd go out and get back from the server, for that folder)
in mind.
Any pointers will be very helpful. Thanks!
Just use Workspace.Get() or overload method (wsp.Get()), it just update updated files.
I don't think we can achieve that. If the files are downloaded to a folder without in source control, there are no versions compared within the folder, even if the folder is in source control, the behavior is just download also no version compare actions. So, it will download all the files ever time and then overwrite the same ones.
In VS, the files are all in TFS source control system, so when we Get Latest Version the changed/added files will be retrieved from TFS. If you want to get the same behavior as VS handles, you can use the tf get command. See Get Command
You can reference this article to use the tf get command :
get-latest-version-of-specific-files-with-tfs-power-tools
Update :-
var tfs = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(new Uri(ConfigurationManager.AppSettings["TFSUrl"]));
tfs.EnsureAuthenticated();
var vsStore = tfs.GetService<VersionControlServer>();
string workingFolder = ConfigurationManager.AppSettings["LocalPathToFolder"]; // C:\TFS\SolutionFolder
string tfsPathToFolder = ConfigurationManager.AppSettings["TFSPathToFolder"]; // $/TFS/SolutionFolder
Workspace wsp = vsStore.GetWorkspace(workingFolder);
if (wsp != null)
{
ItemSpec[] specs = { new ItemSpec(tfsPathToFolder, RecursionType.Full) };
ExtendedItem[][] extendedItems = wsp.GetExtendedItems(specs, DeletedState.NonDeleted, ItemType.Any);
ExtendedItem[] extendedItem = extendedItems[0];
var itemsToDownload = extendedItem.Where(itemToDownload => itemToDownload.IsLatest == false);
foreach (var itemToDownload in itemsToDownload)
{
try
{
switch (itemToDownload.ItemType)
{
case ItemType.File:
if (itemToDownload.LocalItem != null)
{
vsStore.DownloadFile(itemToDownload.SourceServerItem, itemToDownload.LocalItem);
}
else
{
string localItemPath = itemToDownload.SourceServerItem.Replace(tfsPathToFolder,
workingFolder);
vsStore.DownloadFile(itemToDownload.SourceServerItem, localItemPath);
}
break;
case ItemType.Folder:
string folderName = itemToDownload.SourceServerItem.Replace(tfsPathToFolder, workingFolder);
if ((!string.IsNullOrEmpty(folderName)) && (!Directory.Exists(folderName)))
{
Directory.CreateDirectory(folderName);
}
break;
}
}
catch (Exception e)
{
File.AppendAllText(#"C:\TempLocation\GetLatestExceptions.txt", e.Message);
}
}
}
This code works well, except:
a. Whenever it downloads the latest copy of, let's say a file, it 'checks it out' in TFS :(
b. For some items, it throws errors like 'Item $/TFS/SolutionFolder/FolderX/abc.cs was not found in source control at version T.' - I have to find out what the exact cause of this issue is, though.
Any ideas on how to get around these two issues or any other problems you see with this code? Thanks!

Replacing a file with a new file of the same name but different content in TFS via C#

i´m currently working on a programm which updates templates on our companies Team Foundation Server. I am having those new templates locally on my disk and want to replace the existing ones on the server. I was trying different approaches and this is my newest version. The problem is that either
the new file is "in use" when accessing it through coding in c#(while not in use when i try to replace it in runtime using the normal explorer).
the replacement is not appearing in the pending changes, the pendingChanges array is initial.
using (var tfs = TeamFoundationServerFactory.GetServer("myserver"))
{
var versionControlServer = tfs.GetService(typeof(VersionControlServer)) as VersionControlServer;
// Create a new workspace for the currently authenticated user.
var workspace = versionControlServer.CreateWorkspace("Temporary Workspace", versionControlServer.AuthorizedUser);
try
{
// Check if a mapping already exists.
var workingFolder = new WorkingFolder("$serverpath", #"c:\tempFolder");
// Create the mapping (if it exists already, it just overides it, that is fine).
workspace.CreateMapping(workingFolder);
workspace.Get(VersionSpec.Latest, GetOptions.GetAll);
string[] paths = new string[1];
paths[0] = "test.pdf";
workspace.PendEdit(paths, RecursionType.Full, null, LockLevel.None);
// Go through the folder structure defined and create it locally, then check in the changes.
CreateFolderStructure(workspace, workingFolder.LocalItem);
// Check in the changes made.
int a = workspace.CheckIn(workspace.GetPendingChanges(), "This is my comment");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
// Cleanup the workspace.
workspace.Delete();
// Remove the temp folder used.
Directory.Delete(#"C:\tempFolder", true);
}
}
}
static void CreateFolderStructure(Workspace workspace, string initialPath)
{
workspace.PendDelete("$serverpath/test.pdf", RecursionType.None);
File.Copy(#"C:\test\testnew.pdf", #"C:\tempfolder\test", true);
workspace.PendAdd(#"C:\tempfolder\test.pdf");
}
I found a solution to the problem. The workspace which was used by "authorizedUser" was obviously not enough.. I found out that a need a TeamFoundationIdentity to do it. Here is a guide on how to fix the issue.
http://blogs.msdn.com/b/taylaf/archive/2010/03/29/using-tfs-impersonation-with-the-version-control-client-apis.aspx

Upload file to subdirectory with specific name when subdirectories don't exist?

Maybe this is a four-part question:
Upload to subdirectory/ies
Subdirectories don't exist
Use different remote filename than local file
Subdirectories should have explicit permissions (similar to root problem in WinSCP .NET assembly - How to set folder permissions after creating directory?)
Attempted:
var localPath = Path.GetTempFileName();
var remoteFolder = "/some/directory/beneath/root";
var slash = "/"; // maybe given as '/' or '\'...
var remotePath = remoteFolder + slash + "destination.ext1.ext2.txt";
var session = new Session(sessionOptions);
var result = session.PutFiles(localPath, remotePath, false, new FileTransferOptions { FilePermissions = new FilePermissions { Octal = "700" }, KeepTimestamp..., etc });
result.Check();
throws exception Cannot create remote file '/some/directory/beneath/root/destination.ext1.ext2.txt'. ---> WinSCP.SessionRemoteException: No such file or directory.
I was finally able to make the subdirectories with the correct permissions via the crazy workaround indicated here by creating the subdirectory structure in my temp path and using PutFiles on the first folder:
var tempRoot = Path.GetTempPath();
var tempPath = Path.Combine(tempRoot, remoteFolder);
Directory.CreateDirectory(tempPath);
// only need to upload the first segment, PutFiles will magically grab the subfolders too...
var segment = remoteFolder.Substring(0, remoteFolder.IndexOf(slash, StringComparison.Ordinal));
if( !this.DoesFolderExist(segment) )
{
// here's the workaround...
try
{
this._session.PutFiles(Path.Combine(tempRoot, segment), segment, false, new TransferOptions { FilePermissions = this._transferOptions.FilePermissions }).Check();
}
catch (InvalidOperationException)
{
// workaround for bug in .NET assembly prior to 5.5.5/5.6.1 beta
// although I never hit this catch, maybe I've got a new enough version?
}
}
Directory.Delete(tempPath); // finish workaround
but this was way too unintuitive.
ad 1) WinSCP does not (generally) create the target directory of the upload. It must exist prior to the upload. You can test the existence using the Session.FileExists and create the directory using the Session.CreateDirectory, if not. WinSCP, of course, creates the directories you are uploading, if needed.
ad 3) You specify different target name in the remotePath argument of the Session.PutFiles:
session.PutFiles(#"C:\path\original.txt", "/home/user/newname.txt");
ad 4) You specify permissions of uploaded file/directory using the TransferOptions.FilePermissions. Note that WinSCP implicitly adds x permission to directories for every group, where r permission is granted. So when you specify 600 permissions for batch upload, the 600 is used for files, while 700 is used for directories. If you need to use different permissions for different files/directories, you need to upload them one by one.

Programmatically get pending changes for a specific folder in a specific project?

I have been playing around with TFS for a while because I need to upload/check in specific files and folders into various locations on the TFServer after they have been created and placed locally. I am mapping the workspace and everything and I get a bunch of changes from PendingChanges but not the ones I want and not where I want. The problem is really annoying because for every check in process I only want to work on a specific folder or file in a specific location that is already mapped. I am gonna paste part of the code here for reference.
using (TfsTeamProjectCollection collection = new TfsTeamProjectCollection(serverUri, _cred))
{
VersionControlServer versionControl = (VersionControlServer)collection.GetService(typeof(VersionControlServer));
string machineName = Environment.MachineName;
string currentUserName = Environment.UserName;
Workspace myWorkspace = versionControl.GetWorkspace(machineName, currentUserName);
// tried this from Stack but didn't work.
//PendingChange[] changes = myWorkspace.GetPendingChanges().Where(x => x.LocalOrServerFolder.Contains(localPath)).ToArray();
PendingChange[] changes = myWorkspace.GetPendingChanges();
if (changes.Length != 0)
{
foreach (PendingChange c in changes)
{
rt.Text += " path: " + c.LocalItem + ", change: " + PendingChange.GetLocalizedStringForChangeType(c.ChangeType) + "\n";
}
}
else
{
rt.Text += "This didn't work.";
}
}
Basically I want to throw a folder or file at my TFS class and let it check if the file or folder is up to date or present on the TFS and act accordingly. I hope I have described the problem thoroughly.
I think that my answer will not response fully you question, but if you want to get pending changes for a specific folder, you can use something like this..
PendingChange[] changes = myWorkspace.GetPendingChanges(localPath, RecursionType.Full, false);

How do I get build details in a custom workflow activity?

I need to add a custom activity to the default workflow template to increase assembly versions at the earliest point possible in the build process.
What I would like to achieve is to create and map the exact same workspace (that is be created further down in the workflow) inside my custom activity so that I can check out an xml file, increase the version number held within, write it back to the xml file and check the xml file back in.
I'm aware that this workspace will be created later on in the workflow but that will be too late in the build process for what I'm trying to achieve, so instead of moving any of the activities or duplicating them in a position above my custom activity (this should be ok as this workspace will be deleted and recreated again later)
I think the details I need are the BuildDirectory, WorkspaceName and SourcesDirectory. Can anyone tell me how to achieve the creation of the workspace or how obtain this data in code?
the build will be carried out on a build server, and I am using TFS 2010 and c#.
Thanks in advance
I followed the series of blog articles by Ewald Hofman as a primer and created a custom activity that does the check-out, update and check-in of a GlobalAssemblyInfo file that I parse the current version from. My task is inserted at the top of the "Update Drop Location" which is right after it does the "Get the build" portion of the workflow. I just use require the IBuildDetail and a File Mask as arguments from which you can pull out the VersionControlServer to be able to access TFS. My code is below:
protected override string Execute(CodeActivityContext context)
{
// Obtain the runtime value of the input arguments.
string assemblyInfoFileMask = context.GetValue(AssemblyInfoFileMask);
IBuildDetail buildDetail = context.GetValue(BuildDetail);
var workspace = buildDetail.BuildDefinition.Workspace;
var versionControl = buildDetail.BuildServer.TeamProjectCollection.GetService<VersionControlServer>();
Regex regex = new Regex(AttributeKey + VersionRegex);
// Iterate of the folder mappings in the workspace and find the AssemblyInfo files
// that match the mask.
foreach (var folder in workspace.Mappings)
{
string path = Path.Combine(folder.ServerItem, assemblyInfoFileMask);
context.TrackBuildMessage(string.Format("Checking for file: {0}", path));
ItemSet itemSet = versionControl.GetItems(path, RecursionType.Full);
foreach (Item item in itemSet.Items)
{
context.TrackBuildMessage(string.Format("Download {0}", item.ServerItem));
string localFile = Path.GetTempFileName();
try
{
// Download the file and try to extract the version.
item.DownloadFile(localFile);
string text = File.ReadAllText(localFile);
Match match = regex.Match(text);
if (match.Success)
{
string versionNumber = match.Value.Substring(AttributeKey.Length + 2, match.Value.Length - AttributeKey.Length - 4);
Version version = new Version(versionNumber);
Version newVersion = new Version(version.Major, version.Minor, version.Build + 1, version.Revision);
context.TrackBuildMessage(string.Format("Version found {0}", newVersion));
return newVersion.ToString();
}
}
finally
{
File.Delete(localFile);
}
}
}
return null;
}

Categories

Resources