Reputation: 73
How can I iterate through all of the files on a commit/push to a Git repository in TFS? I am writing a server side plugin and need access to the list of files committed during a push. I have the plugin working and getting the commit message but need the list of files as well.
Also, is there a way to modify the commit message once pushed to the server? We want to denote whether the ID (#12345) is a PBI, Task, or Bug
Thanks
Scott
Upvotes: 0
Views: 885
Reputation: 11
How can I iterate through all of the files on a commit/push to a Git repository in TFS? I am writing a server side plugin and need access to the list of files committed during a push. I have the plugin working and getting the commit message but need the list of files as well.
Part of the ISubscriber
interface is the following method:
public EventNotificationStatus ProcessEvent(IVssRequestContext requestContext, NotificationType notificationType, object notificationEventArgs, out int statusCode, out string statusMessage, out ExceptionPropertyCollection properties)
This is the entry point of your Plugin. The notificationEventArgs
parameter here is one of the types you subscribed to. If you subscribe to the Microsoft.TeamFoundation.Git.Server.PushNotification
type, you can get the file names using something like the following:
var repositoryService = requestContext.GetService<ITeamFoundationGitRepositoryService>();
var repository = repositoryService.FindRepositoryById(requestContext, (notificationEventArgs as PushNotification).RepositoryId);
var diffEntries = new List<TfsGitDiffEntry>();
var commits = (notificationEventArgs as PushNotification).IncludedCommits;
foreach(var item in commits)
{
var gitCommit = repository?.LookupObject(item)?.ResolveToCommit();
var manifest = gitCommit.GetManifest(true);
diffEntries.AddRange(manifest.Where(x.NewObjectType == GitObjectType.Blob));
}
var files = diffEntries.Select(d => d.RelativePath).Distinct();
The TfsGitDiffEntry
objects can also be used to determine a variety of things see the class reference for more information: https://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k(Microsoft.TeamFoundation.Git.Server.TfsGitDiffEntry);k(TargetFrameworkMoniker-.NETFramework,Version%3Dv4.6.1);k(DevLang-csharp)&rd=true
Getting the contents of the file from a TfsGitDiffEntry
is as simple as:
var oldFileContentsStream = repository.LookupObject<TfsGitBlob>(diffEntry.OldObjectId.Value).GetContent();
or
var newFileContentsStream = repository.LookupObject<TfsGitBlob>(diffEntry.NewObjectId.Value).GetContent();
Also, is there a way to modify the commit message once pushed to the server? We want to denote whether the ID (#12345) is a PBI, Task, or Bug
In my experimentation, I have not been able to find a way to do this.
Upvotes: 0
Reputation: 487775
How can I iterate through all of the files on a commit/push to a Git repository in TFS? I am writing a server side plugin and need access to the list of files committed during a push. I have the plugin working and getting the commit message but need the list of files as well.
A commit is a complete snapshot of a work tree, so every commit has every file in it. I am sure that's not what you want but it's a key to understanding how to get what you want, and perhaps more importantly, how to figure out what you want for merge commits in particular.
For example, suppose on the server, in a hook, you get a pre- or post-receive input line, or an update hook's arguments, saying:
refs/heads/foo
.c13f98a...
.0b4a13e...
.(This is how the information arrives in a hook. In a pre-receive or post-receive hook, you read a series of lines from standard input and each line has the old and new hash and the reference name, in that order. In an update hook, the three arguments to the script give the reference name and the old and new hashes, in that order. In both pre-receive and update hooks, the update has not happened yet and your script can prohibit it, while in the post-receive hook, the reference has already been updated and you are merely given a second chance to inspect the result.)
At most one of the two hashes may be the all-zeros null hash. In this case, the reference (branch, tag, notes, or whatever it is) is being created or deleted. Otherwise both hashes are valid and the reference is being moved.
If the reference is a branch name—starts with refs/heads/
; the remainder of the name, possibly including more slashes, is the branch name—then the update may make some commit(s) reachable and/or make other commits unreachable from the new hash, i.e., put some commits into and/or remove some commits from that branch. (The same is true for all other updates, including tags, although normally tag updates are simply refused entirely, and normally we don't try to traverse the commit DAG from a tag.)
Note that although some commits become newly reachable or unreachable through this name, there is no guarantee that those commits were entirely unreachable before, or are entirely unreachable now. For instance, suppose the existing commit graph, before your hook is invoked, includes this fragment:
... - E - F - H <-- refs/heads/foo
\ /
G <-- refs/heads/bar
where E
, F
, and H
are commits "on branch foo
" with H
being a merge commit, and E
and G
are commits "on branch bar
". Then you get an update request saying "move bar
to point to commit H
" (so that foo
and bar
will both point to the same commit). Merge commit H
is not new, it merely becomes newly-reachable through branch bar
: it was already reachable through branch foo
. A subsequent force-push that resets bar
to point back to G
(if the original push was in error) makes H
unreachable from bar
, but it remains reachable from foo
.
Thus, depending on precisely what you want, you will generally start by getting and working through a list of commit IDs that have become reachable or unreachable, using git rev-list
, which is designed to do just that:
for i in $(git rev-list $old..$new); ... done # newly reachable
for i in $(git rev-list $new..$old); ... done # newly unreachable
(You might get these lists up front, and perhaps count them.) In a pre-receive hook, you can also check whether any of the other reference updates will make newly-reachable commits reachable (via some other name), and in either a pre-receive or an update hook, you can look at current, not-yet-updated references (using git for-each-ref
for instance, or git rev-list --all
, or whatever you need) to see if newly reachable commits are already reachable by some other name. (In a post-receive hook, you could do the same thing, but it's more difficult since the references are already updated, and you'd have to programmatically compute their pre-update values, including pretending to restore any deleted references.)
Each commit $i
in each list may be an ordinary, non-merge commit, which has a single parent; or it may be a merge commit, which by definition has two or more parents.
For ordinary, non-merge commits, getting the set of changes as compared to its parent is easy: just run git diff
on the two hashes (using --name-only
or --name-status
to get the kind of information I am sure you are looking for).
For merge commits, however, it's more difficult, because a merge has two or more parents. You need to decide what it means if, e.g., file xyzzy
is exactly the same as in one parent of the merge, but different from the other, and whether you care about which parent(s) it differs from. Chances are somewhat good that what you care about is how these files compare to the first parent (which means you can use the same code as for a non-merge, using ${i}^
to obtain commit $i
's first parent for doing the git diff
), but chances are also somewhat good that you need something different or something more here.
(Note that I know nothing of whatever software you already have, but I can say that a lot of hook writers get a lot of these details wrong, because they have not considered (or are not even aware of) the possibility that a force-push might add 5 commits while removing 3, and/or that so-called "evil" merges may sneak in changes that do not appear in any parent (git's combined diffs are designed to find such changes).)
Also, is there a way to modify the commit message once pushed to the server? We want to denote whether the ID (#12345) is a PBI, Task, or Bug
No. It's impossible to modify anything about a commit, because the commit's hash ID (0b4a13e...
) is a cryptographic checksum of the contents of the commit. Change anything, even a single bit, and the checksum changes dramatically and unpredictably (well, "predictable" by doing the checksum of the new content, of course). This is precisely what makes upstream rebases difficult for downstream users: you don't change commits, you copy them to new commits and tell everyone "please forget those old ones and use these new ones instead".
Edit: I forgot to mention that this sort of thing (adding a note to a commit) is what git notes
are designed for. They work not by actually modifying the commit itself, but rather by adding a special sort of bag-on-the-side that git log
consults automatically. The implementation mechanism is quite tricky, but it comes down to the fact that commits are uniquely identified by their hashes (0b4a13e...
, again), so git log
can look at the refs/heads/notes
reference to see if there is additional information for 0b4a13e...
. If so, git log
will add that information to the log, unless you tell it not to (--no-notes
) or point it at a different reference (--notes=refs/notes/foo
, for instance).
Upvotes: 3