using System; using System.Collections.Generic; using System.Collections; using System.IO; using System.Net; using Exortech.NetReflector; using ThoughtWorks.CruiseControl.Core.Util; using Microsoft.TeamFoundation.VersionControl.Client; using Microsoft.TeamFoundation.Client; // Placed this is the same namespace that CCNET uses as hopefully it might end up being distrbuted with CCNET. // TODO: Check that this does not cause any naming conflicts namespace ThoughtWorks.CruiseControl.Core.Sourcecontrol { /// /// Source Control Plugin for CruiseControl.NET that talks to VSTS Team Foundation Server. /// [ReflectorType("vsts")] public class Vsts : ISourceControl { #region Constants private const string DEFAULT_WORKSPACE_NAME = "CCNET"; private const string DEFAULT_WORKSPACE_COMMENT = "Temporary CruiseControl.NET Workspace"; #endregion Constants #region NetReflectored Properties /// /// The name or URL of the team foundation server. For example http://vstsb2:8080 or vstsb2 if it /// has already been registered on the machine. /// [ReflectorProperty("server")] public string Server; /// /// The path to the project in source control, for example $\VSTSPlugins /// [ReflectorProperty("project")] public string ProjectPath; /// /// Gets or sets whether this repository should be labeled. /// [ReflectorProperty("applyLabel", Required = false)] public bool ApplyLabel = false; [ReflectorProperty("autoGetSource", Required = false)] public bool AutoGetSource = false; /// /// Username that should be used. Domain cannot be placed here, rather in domain property. /// [ReflectorProperty("username", Required = false)] public string Username; /// /// The password in clear test of the domain user to be used. /// [ReflectorProperty("password", Required = false)] public string Password; /// /// The domain of the user to be used. /// [ReflectorProperty("domain", Required = false)] public string Domain; [ReflectorProperty("workingDirectory", Required = false)] public string WorkingDirectory; [ReflectorProperty("cleanCopy", Required = false)] public bool CleanCopy = false; [ReflectorProperty("force", Required = false)] public bool Force = false; private string workspaceName; /// /// Name of the workspace to create. This will revert to the DEFAULT_WORKSPACE_NAME if not passed. /// [ReflectorProperty("workspace", Required = false)] public string Workspace { get { if (workspaceName == null) { workspaceName = DEFAULT_WORKSPACE_NAME; } return workspaceName; } set { workspaceName = value; } } [ReflectorProperty("deleteWorkspace", Required = false)] /// /// Flag indicating if workspace should be deleted every time or if it should be /// left (the default). Leaving the workspace will mean that subsequent gets /// will only need to transfer the modified files, improving performance considerably. /// public bool DeleteWorkspace = false; #endregion NetReflectored Properties #region ISourceControl Implementation public Modification[] GetModifications(IIntegrationResult from, IIntegrationResult to) { Log.Debug("Checking Team Foundation Server for Modifications"); Log.Debug("From: " + from.StartTime + " - To: " + to.StartTime); VersionSpec fromVersion = new DateVersionSpec(from.StartTime); VersionSpec toVersion = new DateVersionSpec(to.StartTime); IEnumerable changesets = this.SourceControl.QueryHistory(this.ProjectPath, VersionSpec.Latest, 0, RecursionType.Full, null, fromVersion, toVersion, int.MaxValue, true, false); List modifications = new List(); int latestChangeSetId = 0; // Each changeset contains multiple file modifications. // Build up array of all CCNET modifications from all changesets. foreach (Changeset changeset in changesets) { string userName = changeset.Committer; string comment = changeset.Comment; int changeNumber = changeset.ChangesetId; if (latestChangeSetId < changeNumber) latestChangeSetId = changeNumber; // In VSTS, the version of the file is the same as the changeset number it was checked in with. string version = Convert.ToString(changeNumber); DateTime modifedTime = this.TFS.TimeZone.ToLocalTime(changeset.CreationDate); foreach (Change change in changeset.Changes) { Modification modification = this.ConvertToModification(userName, comment, changeNumber, version, modifedTime, change); modifications.Add(modification); } } if (latestChangeSetId == 0) { changesets = this.SourceControl.QueryHistory(this.ProjectPath, VersionSpec.Latest, 0, RecursionType.Full, null, null, null, 1, false, false); foreach (Changeset changeset in changesets) latestChangeSetId = changeset.ChangesetId; } this.WorkingVersion = new ChangesetVersionSpec(latestChangeSetId); Log.Debug(string.Format("Found {0} modifications", modifications.Count)); return modifications.ToArray(); } public void LabelSourceControl(IIntegrationResult result) { if (ApplyLabel && result.Succeeded) { Log.Debug(String.Format("Applying label \"{0}\"", result.Label)); VersionControlLabel label = new VersionControlLabel(this.SourceControl, result.Label, sourceControl.AuthenticatedUser, this.ProjectPath, "Labeled by CruiseControl.NET"); // Create Label Item Spec. ItemSpec itemSpec = new ItemSpec(this.ProjectPath, RecursionType.Full); LabelItemSpec[] labelItemSpec = new LabelItemSpec[] { new LabelItemSpec(itemSpec, this.WorkingVersion, false) }; this.SourceControl.CreateLabel(label, labelItemSpec, LabelChildOption.Replace); } } public void GetSource(IIntegrationResult result) { result.AddIntegrationProperty("CCNetVSTSChangeSetId", ((ChangesetVersionSpec)this.WorkingVersion).ChangesetId.ToString()); if (AutoGetSource) { if (CleanCopy) { // If we have said we want a clean copy, then delete old copy before getting. Log.Debug("Deleting " + this.WorkingDirectory); this.deleteDirectory(this.WorkingDirectory); } Workspace[] workspaces = this.SourceControl.QueryWorkspaces(Workspace, this.SourceControl.AuthenticatedUser, Workstation.Current.Name); Workspace workspace = null; if (workspaces.Length > 0) { // The workspace exists. if (DeleteWorkspace) { // We have asked for a new workspace every time, therefore delete the existing one. Log.Debug("Removing existing workspace " + Workspace); this.SourceControl.DeleteWorkspace(Workspace, this.SourceControl.AuthenticatedUser); workspaces = new Workspace[0]; } else { Log.Debug("Existing workspace detected - reusing"); workspace = workspaces[0]; } } if (workspaces.Length == 0) { Log.Debug("Creating new workspace name: " + Workspace ); workspace = this.SourceControl.CreateWorkspace(Workspace, this.SourceControl.AuthenticatedUser, DEFAULT_WORKSPACE_COMMENT); } try { workspace.Map(ProjectPath, WorkingDirectory); Log.Debug(String.Format("Getting {0} to {1}", ProjectPath, WorkingDirectory)); GetRequest getReq = new GetRequest(new ItemSpec(ProjectPath, RecursionType.Full), this.WorkingVersion); if (CleanCopy || Force) { Log.Debug("Forcing a Get Specific with the options \"get all files\" and \"overwrite read/write files\""); workspace.Get(getReq, GetOptions.GetAll | GetOptions.Overwrite); } else { Log.Debug("Performing a Get Latest"); workspace.Get(getReq, GetOptions.None); } } finally { if (workspace != null && DeleteWorkspace) { Log.Debug("Deleting the workspace"); workspace.Delete(); } } } } public void Initialize(IProject project) { // Do Nothing } public void Purge(IProject project) { //never called by CCNet, cruft... } #endregion ISourceControl Implementation #region Private Members private Modification ConvertToModification(string userName, string comment, int changeNumber, string version, DateTime modifedTime, Change change) { Modification modification = new Modification(); modification.UserName = userName; modification.Comment = comment; modification.ChangeNumber = changeNumber; modification.ModifiedTime = modifedTime; modification.Version = version; modification.Type = PendingChange.GetLocalizedStringForChangeType(change.ChangeType); // Populate fields from change item Item item = change.Item; if (item.ItemType == ItemType.File) { // split into foldername and filename int lastSlash = item.ServerItem.LastIndexOf('/'); modification.FileName = item.ServerItem.Substring(lastSlash + 1); // patch to the following line submitted by Ralf Kretzschmar. modification.FolderName = item.ServerItem.Substring(0, lastSlash); } else { // TODO - what should filename be if dir?? Empty string or null? modification.FileName = string.Empty; modification.FolderName = item.ServerItem; } return modification; } /// /// Delete a directory, even if it contains readonly files. /// private void deleteDirectory(string path) { if (Directory.Exists(WorkingDirectory)) { this.MarkAllFilesReadWrite(path); Directory.Delete(path, true); } } private void MarkAllFilesReadWrite(string path) { DirectoryInfo dirInfo = new DirectoryInfo(path); FileInfo[] files = dirInfo.GetFiles(); foreach (FileInfo file in files) { file.IsReadOnly = false; } // Now recurse down the directories DirectoryInfo[] dirs = dirInfo.GetDirectories(); foreach (DirectoryInfo dir in dirs) { this.MarkAllFilesReadWrite(dir.FullName); } } #endregion Private Members #region Private Properties private VersionSpec _WorkingVersion; private VersionSpec WorkingVersion { get { return _WorkingVersion; } set { _WorkingVersion = value; } } private TeamFoundationServer teamFoundationServer = null; /// /// Cached instance of TeamFoundationServer. /// public TeamFoundationServer TFS { get { if (null == teamFoundationServer) { teamFoundationServer = new TeamFoundationServer(this.Server, this.Credentials); } return teamFoundationServer; } set { teamFoundationServer = value; } } private NetworkCredential networkCredential; /// /// Network credentials used to interact with TFS. /// public NetworkCredential Credentials { get { if (null == networkCredential) { if (Username != null && Password != null) { if (Domain != null) networkCredential = new NetworkCredential(Username, Password, Domain); else networkCredential = new NetworkCredential(Username, Password); } else { networkCredential = CredentialCache.DefaultNetworkCredentials; } } return networkCredential; } set { networkCredential = value; } } private VersionControlServer sourceControl; /// /// The cached instace of the SourceControl object that we are connected to. /// public VersionControlServer SourceControl { get { if (null == sourceControl) { sourceControl = (VersionControlServer)this.TFS.GetService(typeof(VersionControlServer)); } return sourceControl; } set { sourceControl = value; } } #endregion Private Properties } }