ChatGPT解决这个技术问题 Extra ChatGPT

Team Foundation Server - Moving Source with History

I was wondering what the best approach might be in moving source code, with history, from one Team Project to another Team Project. I am not concerned with work items, reporting, or SharePoint sites, as the system we are going to be restoring from did not use these functionalities. The reason for wanting to move to a different Team Project also is driven by the fact that the original implementation (being restored from a backup that was maintained by a third party) were using a third-party process template that we do not wish to use going forward. We want to start utilizing work item tracking and reporting after the migration is complete.

The TFS Integration Platform seems to be one likely scenario. It can be used to change the process template, according to the documentation. However, I was curious if the tf.exe move syntax might work? Something like:

tf.exe move $/ProjectA $/ProjectB

It is my understanding that this command operates much like a rename operation, whereas moving with the "Move" context menu item in Source Control Explorer is more like a delete and add operation. Also, would the tf.exe move path actually associate the code under the folders with the appropriate Team Project, assuming that $/ProjectA is the root source control folder for one project and $/ProjectB is the root source control folder for the other? The key is to be able to preserve the history, if possible.

Any advice or tips would be greatly appreciated!

Edit - Could branching to another project handle this scenario - much like Microsoft discusses in the Branching Guidance documentation? I think that this could be the answer, since the history would likely be preserved with the branch. However, I do not have access to a Team Foundation Server 2008 instance at the moment to test it.


R
Richard Berg

Move and Rename are aliases. There is absolutely no difference, in any version of TFS, from the command line or the UI.

Both of them preserve history. At least in 2005/2008, you keep the same physical item in the VersionedItem table no matter how often or how drastically the name and/or parent path changes. There is actually no way to get a "fake" rename (delete + add) without a lot of manual work on your part.

However, while this versioning model is very pure in a theoretical sense, it has some practical gotchas. Because different items can occupy the same name at different points in time, TFS needs the full name + version to uniquely identify any inputs you send it. Normally you don't notice this restriction, but once have you renamed items in the system, if you say tf [doSomething] $/newname -version:oldversion then it will get confused and either throw an error or operate on an item you may not have intended. You have to be careful to pass valid combinations (newname+newversion or oldname+oldversion) to ensure commands behave the way you want.

TFS 2010 changes the story somewhat: it's a branch+delete under the covers, causing the itemID to change. Even so, everyday commands like Get and History are "faked" very well; old clients are about 95% compatible. The advantage is that when you have multiple renames in the system and path-based item lookups start to become ambiguous as alluded to above, the server will simply accept the name you specify and run with it. This improves overall system performance and eliminates several traps that unfamiliar users often fell into, at the cost of not being quite as flexible and not preserving history with 100% precision (eg when there are name collisions during a Merge of two branches).

Returning to the problem at hand...

It's not as simple as saying tf rename $/projectA $/projectB. Top level folders in the source control tree are reserved for the Team Project Creation Wizard; you can't run standard tf commands against them. What you need is a script like:

Get-TfsChildItem $/ProjectA |
    select -Skip 1 |  # skip the root dir
    foreach {
        tf rename $_.serveritem $_.serveritem.replace("$/ProjectA", "$/ProjectB")
    }

[of course, you can do it by hand if there aren't too many children under $/ProjectA]

As far as the gotchas I mentioned, I'll elaborate on one right now since looking up old history seems very important to you. Once you checkin the rename, tf history $/ProjectA/somefile.cs will NOT work. By default, tf commands assume version = "latest." Any of these alternatives will the full history you want:

tf history $/ProjectA/somefile.cs;1234 where changeset 1234 was before the move

tf history $/ProjectB/somefile.cs;5678 where changeset 5678 was after the move. Or you could just omit the version.

A final alternative for completeness & debugging purposes:

tf history $/ProjectA/somefile.cs -slotmode. You will only see the changes that happened prior to the move; however you'll also see the history of any other items that may have lived in the $/ProjectA/somefile.cs "slot" prior to or subsequent to the item you moved underneath B.

(In TFS 2010, "slot mode" is the default behavior; there's an -ItemMode option to request that your lookup be traced across history like it was 2008 rather than path-based.)

EDIT - no, branching is not a great alternative. While branching does leave enough metadata in the system to trace the full history to & from ProjectB, it's not terribly user friendly in 2008. Plan to spend a lot of time learning the tf merges command (no UI equivalent). 2010 dramatically improves your ability to visualize changes across multiple branches, but it's still not the clean unified experience you'd get from a Rename.


Wow, Richard. Absolutely fantastic information. I will try it in my test lab tomorrow. You had mentioned scripting because the root is reserved. If I was doing the rename from a folder that was a level below the root, I could omit the script, then - correct? Either way - awesome answer. Thank you for your time!
Yeah. The basic idea is to rename ProjectA\folder1 -> ProjectB\folder1, ProjectA\folder2 -> ProjectB\folder2, etc. The script is just to save time & effort in case you have 50 files & folders in there.
Excellent. Thank you so much for making this clear and easy to understand!
Hi Richard, thanks for your script. I really wonder how I can run this? In my VS Console, I can't seem to run this...
If ProjectA is deleted after the move, won't any history that carried over to ProjectB also disappear from the moved items on ProjectB?
s
solublefish

Richard's answer above is well written and explains the situation well. I did have a couple more practical gotcha to add, however.

In TFS2010, the default behavior makes it seem like moving a file causes you to lose all history from before the move. The command my users are likely to use (and the one used, it seems, by the VS2010 GUI) is:

tf history $/ProjectB/somefile.cs

My users intend to get all the history of somefile.cs, both before and after the move. They want "the history of the code that is currently stored in $/ProjectB/somefile.cs", regardless of the filename at any point in time. Maybe other people see it differently.

The first gotcha is that the GUI that appears for me in VS2010 using TFS2010 initally shows only the history since the move. The least-recent item in the list is the rename operation. It can be expanded with a subtle little drop-down arrow. Underneath is the history from the previous location. If you don't know to look for this, it can look like your history is gone.

The second gotcha is that if you later delete ProjectA (because you've finished the migration to ProjectB, say), the history really is gone. Expanding the drop-down in the history for $/ProjectB/somefile.cs doesn't produce the older history.


Thanks for pointing out the danger of deleting "ProjectA" and how it affects the visual history.
Thanks for pointing out the "least recent" list via the little arrow to the left of the Changeset id. I did think I had lost all my history :-(
d
dave walker

Another option (and I think easier) is to import to Git then export back to TFS using the Git-TF command line tools.

Install Git-TF from binary, Choclatey or source code.

Clone a TFS folder:

git tf clone https://myAcc.visualstudio.com/mycollection $/TeamProjectA/Main --deep

Disassociate the Git Repo from the TFS server by deleting the .Git/tf folder and the .Git/git-tf file.

Configure the new Git Repo to connect to an empty TFS folder.

git tf configure https://myAcc.visualstudio.com/mycollection $/TeamProjectB/Main --deep

Don't forget the --deep

git tf pull

You should get a message at this point "git-tf: this is a newly configured repository. There is nothing to fetch from tfs."

git commit -a -m "merge commit"

git tf checkin --deep


Dear david004. I tried this and the code history is maintained, but all timestamps are set to today and all users are changed into me. Is this how it works or am I missing something?
Yes, that is a nuisance. I do not know how that can be avoided, though I suspect some kind of version control ninja could write a script for TFS that extracts and inserts date and author info.
timestamps in TFS are designed for audit integrity. If you or other mortal tries to tweak dates or times the TFS database will likely become corrupted or otherwise untrusted by the TFS services (rendering it unusable). I expect a TFS master ninja might be able to crack any audit integrity... but do not expect timestamps to be alterable "by normal means" (per ISO 27001 audit/compliance terminology). Please comment here if you prove otherwise (with script sample :-) ). Cheers!
P
Philip Beck

Regarding the original command above:-

Get-TfsChildItem $/ProjectA |
select -Skip 1 |  # skip the root dir
foreach {
    tf rename $_.serveritem $_.serveritem.replace("$/ProjectA", "$/ProjectB")
}

Both the source and target names need to be surrounded by quotes in case there are any spaces in the full pathfilename. I found it difficult to do this on $_.ServerItem as surrounding it with escaped " returns that whole child object and not just the .serverItem string. Or if I did manage to get the string, I got unwanted carriage returns such as

" $proj/folder/file "

Eventually I got the command to work with the following, but I found that the history still doesn't get transferred, which was the whole point! So I believe this command is the direct equivalent of just using a right mouse click in source explorer and selecting rename (or move).

$tfsServerString = "http://machine:8080/tfs/DefaultCollection"
$tfs = Get-TfsServer $tfsServerString
Get-TfsChildItem -server $tfs "$/Dest Project/MyTestProject" | select -Skip 1 | foreach { $sourceName = $_.serveritem; $targetName = $_.serveritem.replace("$/Dest Project/MyTestProject/", "$/Dest Project/Source/StoreControllers/dSprint/dSprint2/") ; ./tf rename `"$sourceName`" `"$targetName`" /login:myUser }

Also note, it requires use of the backtick ` to escape "


关注公众号,不定期副业成功案例分享
Follow WeChat

Success story sharing

Want to stay one step ahead of the latest teleworks?

Subscribe Now