I think Changesets are what you want to use. Though a changeset does identify a specific set of code changes, when you perform operations within TFS based on a changeset, TFS usually interprets it as "all changes up to and including changeset XXX."
So, let's say you ask to branch at changeset 12345
. TFS will get all files that are associated with a changeset less than or equal to 12345
-- even if they aren't part of 12345
.
Another option you can do is branch based on a specific date/time. For example, if you had a major release on June 1st at 11:00AM, you can simply branch all code from that specific date and time.
All that being said, in our organization, we use labels. Yes, labels can be moved, but that's not necessarily a bad thing, as it lets you account for mistakes. For example, let's say we have a label: ProdMove_June
.
Some time after the June release, we realize that, due to a failure of process, a configuration file was not included in TFS (or the file was deployed from TFS, but needed to be changed outside of the SCM process to respond to an emergency). We then move this file into TFS and need to label it for all future branching. If that's the case, then all we need to do is move the label on that single file.
In my opinion, all three of these approaches are valid, and we actually use all three within our organization for building, branching, and getting code. I would suggest that you arm yourself with all three of these on your utility belt and use them where appropriate.