using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using Microsoft.TeamFoundation; using Microsoft.TeamFoundation.Server; using Microsoft.TeamFoundation.VersionControl.Client; using Microsoft.TeamFoundation.WorkItemTracking.Client; using SEP.Extensions; using Sep.Git.Tfs.Commands; using Sep.Git.Tfs.Core; using Sep.Git.Tfs.Core.TfsInterop; using Sep.Git.Tfs.Util; using StructureMap; using ChangeType = Microsoft.TeamFoundation.Server.ChangeType; namespace Sep.Git.Tfs.VsCommon { public abstract class TfsHelperBase : ITfsHelper { private readonly TextWriter _stdout; private readonly TfsApiBridge _bridge; private readonly IContainer _container; public TfsHelperBase(TextWriter stdout, TfsApiBridge bridge, IContainer container) { _stdout = stdout; _bridge = bridge; _container = container; } public abstract string TfsClientLibraryVersion { get; } public string Url { get; set; } public string Username { get; set; } public string Password { get; set; } public bool HasCredentials { get { return !String.IsNullOrEmpty(Username); } } public abstract void EnsureAuthenticated(); private string[] _legacyUrls; protected NetworkCredential GetCredential() { var idx = Username.IndexOf('\\'); if (idx >= 0) { string domain = Username.Substring(0, idx); string login = Username.Substring(idx + 1); return new NetworkCredential(login, Password, domain); } return new NetworkCredential(Username, Password); } protected abstract T GetService(); private VersionControlServer _versionControl; protected VersionControlServer VersionControl { get { if (_versionControl != null) return _versionControl; _versionControl = GetService(); _versionControl.NonFatalError += NonFatalError; _versionControl.Getting += Getting; return _versionControl; } } private WorkItemStore WorkItems { get { return GetService(); } } private void NonFatalError(object sender, ExceptionEventArgs e) { if (e.Failure != null) { _stdout.WriteLine(e.Failure.Message); Trace.WriteLine("Failure: " + e.Failure.Inspect(), "tfs non-fatal error"); } if (e.Exception != null) { _stdout.WriteLine(e.Exception.Message); Trace.WriteLine("Exception: " + e.Exception.Inspect(), "tfs non-fatal error"); } } private void Getting(object sender, GettingEventArgs e) { Trace.WriteLine("get [C" + e.Version + "]" + e.ServerItem); } private IGroupSecurityService GroupSecurityService { get { return GetService(); } } private ILinking _linking; private ILinking Linking { get { return _linking ?? (_linking = GetService()); } } public IEnumerable GetChangesets(string path, long startVersion, GitTfsRemote remote) { var changesets = VersionControl.QueryHistory(path, VersionSpec.Latest, 0, RecursionType.Full, null, new ChangesetVersionSpec((int) startVersion), VersionSpec.Latest, int.MaxValue, true, true, true); return changesets.Cast() .OrderBy(changeset => changeset.ChangesetId) .Select(changeset => BuildTfsChangeset(changeset, remote)); } public virtual bool CanGetBranchInformation { get { return false; } } public virtual IEnumerable GetAllTfsRootBranchesOrderedByCreation() { throw new NotImplementedException(); } public virtual IEnumerable GetBranches() { throw new NotImplementedException(); } public virtual int GetRootChangesetForBranch(string tfsPathBranchToCreate, string tfsPathParentBranch = null) { Trace.WriteLine("TFS 2008 Compatible mode!"); int firstChangesetIdOfParentBranch = 1; if (string.IsNullOrWhiteSpace(tfsPathParentBranch)) throw new GitTfsException("This version of TFS Server doesn't permit to use this command :(\nTry using option '--parent-branch'..."); var changesetIdsFirstChangesetInMainBranch = VersionControl.GetMergeCandidates(tfsPathParentBranch, tfsPathBranchToCreate, RecursionType.Full).Select(c => c.Changeset.ChangesetId).FirstOrDefault(); if (changesetIdsFirstChangesetInMainBranch == 0) { Trace.WriteLine("No changeset in main branch since branch done... (need only to find the last changeset in the main branch)"); return VersionControl.QueryHistory(tfsPathParentBranch, VersionSpec.Latest, 0, RecursionType.Full, null, new ChangesetVersionSpec(firstChangesetIdOfParentBranch), VersionSpec.Latest, 1, false, false).Cast().First().ChangesetId; } Trace.WriteLine("First changeset in the main branch after branching : " + changesetIdsFirstChangesetInMainBranch); Trace.WriteLine("Try to find the previous changeset..."); int step = 100; int upperBound = changesetIdsFirstChangesetInMainBranch - 1; int lowerBound = Math.Max(upperBound - step, 1); //for optimization, retrieve the lesser possible changesets... so 100 by 100 while (true) { Trace.WriteLine("Looking for the changeset between changeset id " + lowerBound + " and " + upperBound); var firstBranchChangesetIds = VersionControl.QueryHistory(tfsPathParentBranch, VersionSpec.Latest, 0, RecursionType.Full, null, new ChangesetVersionSpec(lowerBound), new ChangesetVersionSpec(upperBound), int.MaxValue, true, false, false).Cast().Select(c => c.ChangesetId).ToList(); if (firstBranchChangesetIds.Count != 0) return firstBranchChangesetIds.First(cId => cId < changesetIdsFirstChangesetInMainBranch); else { if (upperBound == 1) { throw new GitTfsException("An unexpected error occured when trying to find the root changeset.\nFailed to find a previous changeset to changeset n°" + changesetIdsFirstChangesetInMainBranch + " in the branch!!!"); } upperBound = Math.Max(upperBound - step, 1); lowerBound = Math.Max(upperBound - step, 1); } } } private ITfsChangeset BuildTfsChangeset(Changeset changeset, GitTfsRemote remote) { var tfsChangeset = _container.With(this).With(_bridge.Wrap(changeset)).GetInstance(); tfsChangeset.Summary = new TfsChangesetInfo { ChangesetId = changeset.ChangesetId, Remote = remote }; if (changeset.WorkItems != null) { tfsChangeset.Summary.Workitems = changeset.WorkItems.Select(wi => new TfsWorkitem { Id = wi.Id, Title = wi.Title, Description = wi.Description, Url = Linking.GetArtifactUrl(wi.Uri.AbsoluteUri) }); } return tfsChangeset; } public void WithWorkspace(string localDirectory, IGitTfsRemote remote, TfsChangesetInfo versionToFetch, Action action) { Trace.WriteLine("Setting up a TFS workspace at " + localDirectory); var workspace = GetWorkspace(localDirectory, remote.TfsRepositoryPath); try { var tfsWorkspace = _container.With("localDirectory").EqualTo(localDirectory) .With("remote").EqualTo(remote) .With("contextVersion").EqualTo(versionToFetch) .With("workspace").EqualTo(_bridge.Wrap(workspace)) .With("tfsHelper").EqualTo(this) .GetInstance(); action(tfsWorkspace); } finally { workspace.Delete(); } } private Workspace GetWorkspace(string localDirectory, string repositoryPath) { var workspace = VersionControl.CreateWorkspace(GenerateWorkspaceName()); try { workspace.CreateMapping(new WorkingFolder(repositoryPath, localDirectory)); } catch (MappingConflictException e) { workspace.Delete(); throw new GitTfsException(e.Message).WithRecommendation("Run 'git tfs cleanup-workspaces' to remove the workspace."); } catch { workspace.Delete(); throw; } return workspace; } private string GenerateWorkspaceName() { return "git-tfs-" + Guid.NewGuid(); } public abstract long ShowCheckinDialog(IWorkspace workspace, IPendingChange[] pendingChanges, IEnumerable checkedInfos, string checkinComment); public void CleanupWorkspaces(string workingDirectory) { // workingDirectory is the path to a TFS workspace managed by git-tfs. // By default, this will be something like `.git/tfs/default/workspace`. // If `git-tfs.workspace-dir` is set, workingDirectory will be that path. Trace.WriteLine("Looking for workspaces mapped to @\"" + workingDirectory + "\"...", "cleanup-workspaces"); var workspace = VersionControl.TryGetWorkspace(workingDirectory); if (workspace != null) { Trace.WriteLine("Found mapping in workspace \"" + workspace.DisplayName + "\".", "cleanup-workspaces"); if (workspace.Folders.Length == 1) { // Normally, the workspace will have one mapping, mapped to the git-tfs // workspace folder. In that case, we just delete the workspace. _stdout.WriteLine("Removing workspace \"" + workspace.DisplayName + "\"."); workspace.Delete(); } else { // If something outside of git-tfs set up a workspace, the workspace // might be set at a higher directory. If there is more than one mapping // in the workspace, we only need to remove the one that includes the working // directory that we want to set. var fullWorkingDirectoryPath = Path.GetFullPath(workingDirectory); foreach (var mapping in workspace.Folders.Where(f => fullWorkingDirectoryPath.StartsWith(Path.GetFullPath(f.LocalItem), StringComparison.CurrentCultureIgnoreCase))) { _stdout.WriteLine("Removing @\"" + mapping.LocalItem + "\" from workspace \"" + workspace.DisplayName + "\"."); workspace.DeleteMapping(mapping); } } } } public bool HasShelveset(string shelvesetName) { var matchingShelvesets = VersionControl.QueryShelvesets(shelvesetName, GetAuthenticatedUser()); return matchingShelvesets != null && matchingShelvesets.Length > 0; } protected abstract string GetAuthenticatedUser(); public abstract bool CanShowCheckinDialog { get; } public ITfsChangeset GetShelvesetData(IGitTfsRemote remote, string shelvesetOwner, string shelvesetName) { shelvesetOwner = shelvesetOwner == "all" ? null : (shelvesetOwner ?? GetAuthenticatedUser()); var shelvesets = VersionControl.QueryShelvesets(shelvesetName, shelvesetOwner); if (shelvesets.Length != 1) { throw new GitTfsException("Unable to find " + shelvesetOwner + "'s shelveset \"" + shelvesetName + "\" (" + shelvesets.Length + " matches).") .WithRecommendation("Try providing the shelveset owner."); } var shelveset = shelvesets.First(); var change = VersionControl.QueryShelvedChanges(shelveset).Single(); var wrapperForVersionControlServer = _bridge.Wrap(VersionControl); // TODO - containerify this (no `new`)! var fakeChangeset = new Unshelveable(shelveset, change, wrapperForVersionControlServer, _bridge); var tfsChangeset = new TfsChangeset(remote.Tfs, fakeChangeset, _stdout, null) { Summary = new TfsChangesetInfo { Remote = remote } }; return tfsChangeset; } public int ListShelvesets(ShelveList shelveList, IGitTfsRemote remote) { var shelvesetOwner = shelveList.Owner == "all" ? null : (shelveList.Owner ?? GetAuthenticatedUser()); IEnumerable shelvesets; try { shelvesets = VersionControl.QueryShelvesets(null, shelvesetOwner); } catch(IdentityNotFoundException e) { _stdout.WriteLine("User '{0}' not found", shelveList.Owner); return GitTfsExitCodes.InvalidArguments; } if (shelvesets.Empty()) { _stdout.WriteLine("No changesets found."); return GitTfsExitCodes.OK; } string sortBy = shelveList.SortBy; if (sortBy != null) { switch (sortBy.ToLowerInvariant()) { case "date": shelvesets = shelvesets.OrderBy(s => s.CreationDate); break; case "owner": shelvesets = shelvesets.OrderBy(s => s.OwnerName).ThenBy(s => s.CreationDate); break; case "name": shelvesets = shelvesets.OrderBy(s => s.Name); break; case "comment": shelvesets = shelvesets.OrderBy(s => s.Comment); break; default: _stdout.WriteLine("ERROR: sorting criteria '{0}' is invalid. Possible values are: date, owner, name, comment", sortBy); return GitTfsExitCodes.InvalidArguments; } } else shelvesets = shelvesets.OrderBy(s => s.CreationDate); if (shelveList.FullFormat) WriteShelvesetsToStdoutDetailed(shelvesets); else WriteShelvesetsToStdout(shelvesets); return GitTfsExitCodes.OK; } private void WriteShelvesetsToStdout(IEnumerable shelvesets) { foreach (var shelveset in shelvesets) { _stdout.WriteLine("{0,-22} {1,-20}", shelveset.OwnerName, shelveset.Name); } } private void WriteShelvesetsToStdoutDetailed(IEnumerable shelvesets) { foreach (var shelveset in shelvesets) { _stdout.WriteLine("Name : {0}", shelveset.Name); _stdout.WriteLine("Owner : {0}", shelveset.OwnerName); _stdout.WriteLine("Date : {0:g}", shelveset.CreationDate); _stdout.WriteLine("Comment: {0}", shelveset.Comment); _stdout.WriteLine(); } } #region Fake classes for unshelve private class Unshelveable : IChangeset { private readonly Shelveset _shelveset; private readonly PendingSet _pendingSet; private readonly IVersionControlServer _versionControlServer; private readonly TfsApiBridge _bridge; private readonly IChange[] _changes; public Unshelveable(Shelveset shelveset, PendingSet pendingSet, IVersionControlServer versionControlServer, TfsApiBridge bridge) { _shelveset = shelveset; _versionControlServer = versionControlServer; _bridge = bridge; _pendingSet = pendingSet; _changes = _pendingSet.PendingChanges.Select(x => new UnshelveChange(x, _bridge, versionControlServer)).Cast().ToArray(); } public IChange[] Changes { get { return _changes; } } public string Committer { get { return _pendingSet.OwnerName; } } public DateTime CreationDate { get { return _shelveset.CreationDate; } } public string Comment { get { return _shelveset.Comment; } } public int ChangesetId { get { return -1; } } public IVersionControlServer VersionControlServer { get { return _versionControlServer; } } public void Get(IWorkspace workspace) { foreach (var change in _changes) { var item = (UnshelveItem)change.Item; item.Get(workspace); } } } private class UnshelveChange : IChange { private readonly PendingChange _pendingChange; private readonly TfsApiBridge _bridge; private readonly UnshelveItem _fakeItem; public UnshelveChange(PendingChange pendingChange, TfsApiBridge bridge, IVersionControlServer versionControlServer) { _pendingChange = pendingChange; _bridge = bridge; _fakeItem = new UnshelveItem(_pendingChange, _bridge, versionControlServer); } public TfsChangeType ChangeType { get { return _bridge.Convert(_pendingChange.ChangeType); } } public IItem Item { get { return _fakeItem; } } } private class UnshelveItem : IItem { private readonly PendingChange _pendingChange; private readonly TfsApiBridge _bridge; private readonly IVersionControlServer _versionControlServer; private long _contentLength = -1; public UnshelveItem(PendingChange pendingChange, TfsApiBridge bridge, IVersionControlServer versionControlServer) { _pendingChange = pendingChange; _bridge = bridge; _versionControlServer = versionControlServer; } public IVersionControlServer VersionControlServer { get { return _versionControlServer; } } public int ChangesetId { get { // some operations like applying rename gets previous item state // via looking at version of item minus 1. So will try to emulate // that this shelve is real revision. return _pendingChange.Version + 1; } } public string ServerItem { get { return _pendingChange.ServerItem; } } public int DeletionId { get { return _pendingChange.DeletionId; } } public TfsItemType ItemType { get { return _bridge.Convert(_pendingChange.ItemType); } } public int ItemId { get { return _pendingChange.ItemId; } } public long ContentLength { get { if (_contentLength < 0) throw new InvalidOperationException("You can't query ContentLength before downloading the file"); // It is not great solution, but at least makes the contract explicit. // We can't actually save downloaded file in this class, because if nobody asks // for it - we won't know when it is safe to delete it and it will stay in the // system forever, which is bad. Implementing finalizer to delete file is also bad solution: // suppose process was killed in the middle of many-megabyte operation on thousands of files // if we delete them as soon as they are not used - only current file will remain. Otherwise // all of them. // With this exception at least it would be evident asap that something went wrong, so we could fix it. return _contentLength; } } public TemporaryFile DownloadFile() { var temp = new TemporaryFile(); _pendingChange.DownloadShelvedFile(temp); _contentLength = new FileInfo(temp).Length; return temp; } public void Get(IWorkspace workspace) { _pendingChange.DownloadShelvedFile(workspace.GetLocalItemForServerItem(_pendingChange.ServerItem)); } } #endregion public IShelveset CreateShelveset(IWorkspace workspace, string shelvesetName) { var shelveset = new Shelveset(_bridge.Unwrap(workspace).VersionControlServer, shelvesetName, workspace.OwnerName); return _bridge.Wrap(shelveset); } public IIdentity GetIdentity(string username) { return _bridge.Wrap(GroupSecurityService.ReadIdentity(SearchFactor.AccountName, username, QueryMembership.None)); } public ITfsChangeset GetLatestChangeset(GitTfsRemote remote) { var history = VersionControl.QueryHistory(remote.TfsRepositoryPath, VersionSpec.Latest, 0, RecursionType.Full, null, null, VersionSpec.Latest, 1, true, false, false).Cast().ToList(); if (history.Empty()) throw new GitTfsException("error: remote TFS repository path was not found"); return BuildTfsChangeset(history.Single(), remote); } public IChangeset GetChangeset(int changesetId) { return _bridge.Wrap(VersionControl.GetChangeset(changesetId)); } public ITfsChangeset GetChangeset(int changesetId, GitTfsRemote remote) { return BuildTfsChangeset(VersionControl.GetChangeset(changesetId), remote); } public IEnumerable GetWorkItemInfos(IEnumerable workItems, TfsWorkItemCheckinAction checkinAction) { return GetWorkItemInfosHelper( workItems, checkinAction, GetWorkItemInfo); } public IEnumerable GetWorkItemCheckedInfos(IEnumerable workItems, TfsWorkItemCheckinAction checkinAction) { return GetWorkItemInfosHelper( workItems, checkinAction, GetWorkItemCheckedInfo); } public ICheckinNote CreateCheckinNote(Dictionary checkinNotes) { if (checkinNotes.IsEmpty()) { return null; } var index = 0; var values = new CheckinNoteFieldValue[checkinNotes.Count]; foreach (var pair in checkinNotes) { values[index++] = new CheckinNoteFieldValue(pair.Key, pair.Value); } return _bridge.Wrap(new CheckinNote(values)); } private IEnumerable GetWorkItemInfosHelper( IEnumerable workItems, TfsWorkItemCheckinAction checkinAction, Func func ) where TWrapper : class { return (from workItem in workItems select _bridge.Wrap( func(workItem, _bridge.Convert(checkinAction)))) .Cast(); } private WorkItemCheckinInfo GetWorkItemInfo(string workItem, WorkItemCheckinAction checkinAction) { return new WorkItemCheckinInfo(WorkItems.GetWorkItem(Convert.ToInt32(workItem)), checkinAction); } private static WorkItemCheckedInfo GetWorkItemCheckedInfo(string workitem, WorkItemCheckinAction checkinAction) { return new WorkItemCheckedInfo(Convert.ToInt32(workitem), true, checkinAction); } public IEnumerable GetLabels(string tfsPathBranch, string nameFilter = null) { var labels = VersionControl.QueryLabels(nameFilter, tfsPathBranch, null, true, tfsPathBranch, VersionSpec.Latest); return labels.Select(e => new TfsLabel { Id = e.LabelId, Name = e.Name, Comment = e.Comment, ChangesetId = e.Items.Where(i=>i.ServerItem.IndexOf(tfsPathBranch) == 0).OrderByDescending(i=>i.ChangesetId).First().ChangesetId, Owner = e.OwnerName, Date = e.LastModifiedDate, IsTransBranch = (e.Items.FirstOrDefault(i => i.ServerItem.IndexOf(tfsPathBranch) != 0) != null) }); } public virtual void CreateBranch(string sourcePath, string targetPath, int changesetId, string comment = null) { throw new NotImplementedException(); } public bool IsExistingInTfs(string path) { return VersionControl.ServerItemExists(path, ItemType.Any); } } }