private bool ReplayRevision(VssPathMapper pathMapper, Revision revision, GitWrapper git, LinkedList<Revision> labels) { var needCommit = false; var actionType = revision.Action.Type; if (revision.Item.IsProject) { // note that project path (and therefore target path) can be // null if a project was moved and its original location was // subsequently destroyed var project = revision.Item; var projectName = project.LogicalName; var projectPath = pathMapper.GetProjectPath(project.PhysicalName); var projectDesc = projectPath; if (projectPath == null) { projectDesc = revision.Item.ToString(); logger.WriteLine("NOTE: {0} is currently unmapped", project); } VssItemName target = null; string targetPath = null; var namedAction = revision.Action as VssNamedAction; if (namedAction != null) { target = namedAction.Name; if (projectPath != null) { targetPath = Path.Combine(projectPath, target.LogicalName); } } bool isAddAction = false; bool writeProject = false; bool writeFile = false; VssItemInfo itemInfo = null; switch (actionType) { case VssActionType.Label: // defer tagging until after commit labels.AddLast(revision); break; case VssActionType.Create: // ignored; items are actually created when added to a project break; case VssActionType.Add: case VssActionType.Share: logger.WriteLine("{0}: {1} {2}", projectDesc, actionType, target.LogicalName); itemInfo = pathMapper.AddItem(project, target); isAddAction = true; break; case VssActionType.Recover: logger.WriteLine("{0}: {1} {2}", projectDesc, actionType, target.LogicalName); itemInfo = pathMapper.RecoverItem(project, target); isAddAction = true; break; case VssActionType.Delete: case VssActionType.Destroy: { logger.WriteLine("{0}: {1} {2}", projectDesc, actionType, target.LogicalName); itemInfo = pathMapper.DeleteItem(project, target); if (targetPath != null && !itemInfo.Destroyed) { if (target.IsProject) { if (Directory.Exists(targetPath)) { if (((VssProjectInfo)itemInfo).ContainsFiles()) { git.Remove(targetPath, true); needCommit = true; } else { // git doesn't care about directories with no files Directory.Delete(targetPath, true); } } } else { if (File.Exists(targetPath)) { // not sure how it can happen, but a project can evidently // contain another file with the same logical name, so check // that this is not the case before deleting the file if (pathMapper.ProjectContainsLogicalName(project, target)) { logger.WriteLine("NOTE: {0} contains another file named {1}; not deleting file", projectDesc, target.LogicalName); } else { File.Delete(targetPath); needCommit = true; } } } } } break; case VssActionType.Rename: { var renameAction = (VssRenameAction)revision.Action; logger.WriteLine("{0}: {1} {2} to {3}", projectDesc, actionType, renameAction.OriginalName, target.LogicalName); itemInfo = pathMapper.RenameItem(target); if (targetPath != null && !itemInfo.Destroyed) { var sourcePath = Path.Combine(projectPath, renameAction.OriginalName); if (target.IsProject ? Directory.Exists(sourcePath) : File.Exists(sourcePath)) { // renaming a file or a project that contains files? var projectInfo = itemInfo as VssProjectInfo; if (projectInfo == null || projectInfo.ContainsFiles()) { CaseSensitiveRename(sourcePath, targetPath, git.Move); needCommit = true; } else { // git doesn't care about directories with no files CaseSensitiveRename(sourcePath, targetPath, Directory.Move); } } else { logger.WriteLine("NOTE: Skipping rename because {0} does not exist", sourcePath); } } } break; case VssActionType.MoveFrom: // if both MoveFrom & MoveTo are present (e.g. // one of them has not been destroyed), only one // can succeed, so check that the source exists { var moveFromAction = (VssMoveFromAction)revision.Action; logger.WriteLine("{0}: Move from {1} to {2}", projectDesc, moveFromAction.OriginalProject, targetPath ?? target.LogicalName); var sourcePath = pathMapper.GetProjectPath(target.PhysicalName); var projectInfo = pathMapper.MoveProjectFrom( project, target, moveFromAction.OriginalProject); if (targetPath != null && !projectInfo.Destroyed) { if (sourcePath != null && Directory.Exists(sourcePath)) { if (projectInfo.ContainsFiles()) { git.Move(sourcePath, targetPath); needCommit = true; } else { // git doesn't care about directories with no files Directory.Move(sourcePath, targetPath); } } else { // project was moved from a now-destroyed project writeProject = true; } } } break; case VssActionType.MoveTo: { // handle actual moves in MoveFrom; this just does cleanup of destroyed projects var moveToAction = (VssMoveToAction)revision.Action; logger.WriteLine("{0}: Move to {1} from {2}", projectDesc, moveToAction.NewProject, targetPath ?? target.LogicalName); var projectInfo = pathMapper.MoveProjectTo( project, target, moveToAction.NewProject); if (projectInfo.Destroyed && targetPath != null && Directory.Exists(targetPath)) { // project was moved to a now-destroyed project; remove empty directory Directory.Delete(targetPath, true); } } break; case VssActionType.Pin: { var pinAction = (VssPinAction)revision.Action; if (pinAction.Pinned) { logger.WriteLine("{0}: Pin {1}", projectDesc, target.LogicalName); itemInfo = pathMapper.PinItem(project, target); } else { logger.WriteLine("{0}: Unpin {1}", projectDesc, target.LogicalName); itemInfo = pathMapper.UnpinItem(project, target); writeFile = !itemInfo.Destroyed; } } break; case VssActionType.Branch: { var branchAction = (VssBranchAction)revision.Action; logger.WriteLine("{0}: {1} {2}", projectDesc, actionType, target.LogicalName); itemInfo = pathMapper.BranchFile(project, target, branchAction.Source); // branching within the project might happen after branching of the file writeFile = true; } break; case VssActionType.Archive: // currently ignored { var archiveAction = (VssArchiveAction)revision.Action; logger.WriteLine("{0}: Archive {1} to {2} (ignored)", projectDesc, target.LogicalName, archiveAction.ArchivePath); } break; case VssActionType.Restore: { var restoreAction = (VssRestoreAction)revision.Action; logger.WriteLine("{0}: Restore {1} from archive {2}", projectDesc, target.LogicalName, restoreAction.ArchivePath); itemInfo = pathMapper.AddItem(project, target); isAddAction = true; } break; } if (targetPath != null) { if (isAddAction) { if (revisionAnalyzer.IsDestroyed(target.PhysicalName) && !database.ItemExists(target.PhysicalName)) { logger.WriteLine("NOTE: Skipping destroyed file: {0}", targetPath); itemInfo.Destroyed = true; } else if (target.IsProject) { Directory.CreateDirectory(targetPath); writeProject = true; } else { writeFile = true; } } if (writeProject && pathMapper.IsProjectRooted(target.PhysicalName)) { // create all contained subdirectories foreach (var projectInfo in pathMapper.GetAllProjects(target.PhysicalName)) { logger.WriteLine("{0}: Creating subdirectory {1}", projectDesc, projectInfo.LogicalName); Directory.CreateDirectory(projectInfo.GetPath()); } // write current rev of all contained files foreach (var fileInfo in pathMapper.GetAllFiles(target.PhysicalName)) { if (WriteRevision(pathMapper, actionType, fileInfo.PhysicalName, fileInfo.Version, target.PhysicalName, git)) { // one or more files were written needCommit = true; } } } else if (writeFile) { // write current rev to working path int version = pathMapper.GetFileVersion(target.PhysicalName); if (WriteRevisionTo(target.PhysicalName, version, targetPath)) { // add file explicitly, so it is visible to subsequent git operations git.Add(targetPath); needCommit = true; } } } } // item is a file, not a project else if (actionType == VssActionType.Edit || actionType == VssActionType.Branch) { // if the action is Branch, the following code is necessary only if the item // was branched from a file that is not part of the migration subset; it will // make sure we start with the correct revision instead of the first revision var target = revision.Item; // update current rev pathMapper.SetFileVersion(target, revision.Version); // write current rev to all sharing projects WriteRevision(pathMapper, actionType, target.PhysicalName, revision.Version, null, git); needCommit = true; } return needCommit; }
private void ProcessItem(VssItem item, string path, PathMatcher exclusionMatcher) { try { foreach (VssRevision vssRevision in item.Revisions) { var actionType = vssRevision.Action.Type; var namedAction = vssRevision.Action as VssNamedAction; if (namedAction != null) { if (actionType == VssActionType.Destroy) { // track destroyed files so missing history can be anticipated // (note that Destroy actions on shared files simply delete // that copy, so destroyed files can't be completely ignored) destroyedFiles.Add(namedAction.Name.PhysicalName); } var targetPath = path + VssDatabase.ProjectSeparator + namedAction.Name.LogicalName; if (exclusionMatcher != null && exclusionMatcher.Matches(targetPath)) { // project action targets an excluded file continue; } } Revision revision = new Revision(vssRevision.DateTime, vssRevision.User, item.ItemName, vssRevision.Version, vssRevision.Comment, vssRevision.Action); LinkedList<Revision> revisionSet; if (!sortedRevisions.TryGetValue(vssRevision.DateTime, out revisionSet)) { revisionSet = new LinkedList<Revision>(); sortedRevisions[vssRevision.DateTime] = revisionSet; } ((ICollection<Revision>)revisionSet).Add(revision); ++revisionCount; } } catch (RecordException e) { var message = string.Format("Failed to read revisions for {0} ({1}): {2}", path, item.PhysicalName, ExceptionFormatter.Format(e)); LogException(e, message); ReportError(message); } }
private Revision FindCorrespondingAction(Revision rev, VssActionType actionType) { if (rev.Action.Type != VssActionType.Edit && rev.Action.Type != VssActionType.Label) { var action = rev.Action as VssNamedAction; for (var node = revisionAnalyzer.SortedRevisions[rev.DateTime].Last; node != revisionAnalyzer.SortedRevisions[rev.DateTime].First.Previous; node = node.Previous) { var revision = node.Value; if (revision.Action.Type != VssActionType.Edit && revision.Action.Type != VssActionType.Label) { var targetaction = revision.Action as VssNamedAction; if (action.Name.PhysicalName == targetaction.Name.PhysicalName && targetaction.Type == actionType && revision.Comment != null) { return(revision); } } } // look into sortedrevisions for the comment var foundPrevKey = false; var foundNextKey = false; var prevKey = (DateTime)DateTime.MinValue; var nextKey = (DateTime)DateTime.MinValue; foreach (var key in revisionAnalyzer.SortedRevisions.Keys) { if (key == rev.DateTime) { foundPrevKey = true; } else if (foundPrevKey) { foundNextKey = true; nextKey = key; break; } if (!foundPrevKey) { prevKey = key; } } if (foundPrevKey && rev.DateTime - prevKey < TimeSpan.FromMinutes(15)) { for (var node = revisionAnalyzer.SortedRevisions[prevKey].Last; node != revisionAnalyzer.SortedRevisions[prevKey].First.Previous; node = node.Previous) { var revision = node.Value; if (revision.Action.Type != VssActionType.Edit && revision.Action.Type != VssActionType.Label) { var targetaction = revision.Action as VssNamedAction; if (action.Name.PhysicalName == targetaction.Name.PhysicalName && targetaction.Type == actionType && revision.Comment != null) { return(revision); } } } } if (foundNextKey && nextKey - rev.DateTime < TimeSpan.FromMinutes(15)) { for (var node = revisionAnalyzer.SortedRevisions[nextKey].First; node != revisionAnalyzer.SortedRevisions[nextKey].Last.Next; node = node.Next) { var revision = node.Value; if (revision.Action.Type != VssActionType.Edit && revision.Action.Type != VssActionType.Label) { var targetaction = revision.Action as VssNamedAction; if (action.Name.PhysicalName == targetaction.Name.PhysicalName && targetaction.Type == actionType && revision.Comment != null) { return(revision); } } } } } return(null); }
public void BuildChangesets() { workQueue.AddLast(delegate(object work) { logger.WriteSectionSeparator(); LogStatus(work, "Building changesets"); var unnamedLabelCount = 0ul; var stopwatch = Stopwatch.StartNew(); var pendingChangesByUser = new Dictionary <string, Changeset>(); foreach (var dateEntry in revisionAnalyzer.SortedRevisions) { var dateTime = dateEntry.Key; //foreach (Revision revision in dateEntry.Value) for (var node = dateEntry.Value.First; node != dateEntry.Value.Last.Next; node = node.Next) { var revision = node.Value; // VSS Destroyed Items: skip all. This will enable merging revisions separated by destroyed items. if (excludeAllDestroyedItems) { if ((revision.Action as VssNamedAction)?.Name?.PhysicalName != null && revisionAnalyzer.IsDestroyed((revision.Action as VssNamedAction).Name.PhysicalName) && !revisionAnalyzer.Database.ItemExists((revision.Action as VssNamedAction).Name.PhysicalName)) { continue; } } // VSS Adds: fill in comments for add from create action if (revision.Action.Type == VssActionType.Add && revision.Comment == null) { node.Value = revision = new Revision(revision.DateTime, revision.User, revision.Item, revision.Version, FindCorrespondingAction(revision, VssActionType.Create)?.Comment, revision.Action); } // VSS Labels: fill in empty label names as svn cannot have empty tag path. if (revision.Action.Type == VssActionType.Label && string.IsNullOrEmpty(((VssLabelAction)revision.Action).Label)) { node.Value = revision = new Revision(revision.DateTime, revision.User, revision.Item, revision.Version, revision.Comment, new VssLabelAction(string.Format(unnamedLabelFormat, revision.DateTime, ++unnamedLabelCount))); } // VSS Creates: skip all and exclude from changeset as it increases history complexity unnecessarily and the info is not useful at all. not used in exporter if (revision.Action.Type == VssActionType.Create) { continue; } // determine target of project revisions var actionType = revision.Action.Type; var namedAction = revision.Action as VssNamedAction; var targetFile = revision.Item.PhysicalName; if (namedAction != null) { targetFile = namedAction.Name.PhysicalName; } // Create actions are only used to obtain initial item comments; // items are actually created when added to a project var creating = (actionType == VssActionType.Create || (actionType == VssActionType.Branch && !revision.Item.IsProject)); // Share actions are never conflict (which is important, // since Share always precedes Branch) var nonconflicting = creating || (actionType == VssActionType.Share); // look up the pending change for user of this revision // and flush changes past time threshold var pendingUser = revision.User; Changeset pendingChange = null; LinkedList <string> flushedUsers = new LinkedList <string>(); foreach (var userEntry in pendingChangesByUser) { var user = userEntry.Key; var change = userEntry.Value; // flush change if file conflict or past time threshold var flush = false; var timeDiff = revision.DateTime - change.DateTime; if (user == pendingUser) { // VSS Label: make labels on their own changeset if ((revision.Action.Type == VssActionType.Label && change.Revisions.Last.Value.Action.Type != VssActionType.Label) || (revision.Action.Type != VssActionType.Label && change.Revisions.Last.Value.Action.Type == VssActionType.Label)) { logger.WriteLine("NOTE: Splitting changeset due to label: {0}", change.Revisions.Last.Value.Action); flush = true; } // Cannot combine due to file conflict. must be recorded as separate changes else if (!nonconflicting && change.TargetFiles.Contains(targetFile)) { logger.WriteLine("NOTE: Splitting changeset due to file conflict on {0}:", targetFile); flush = true; } } else { } /* * if (!mergeAcrossDifferentUser && !HasSameUser(revision, change.Revisions.Last.Value)) * { * logger.WriteLine("NOTE: Splitting changeset due to different user: {0} != {1}", change.Revisions.Last.Value.User, revision.User); * flush = true; * } */ // additional check if not flushed above if (!flush) { if ((TimeSpan.Zero == anyCommentThreshold ? TimeSpan.FromSeconds(1) : TimeSpan.Zero) + timeDiff > anyCommentThreshold) { var lastRevision = FindLastRevisionWithNonEmptyComment(change); //if (HasSameComment(revision, change.Revisions.Last.Value)) if (HasSameComment(revision, lastRevision)) { string message; if ((TimeSpan.Zero == sameCommentThreshold ? TimeSpan.FromSeconds(1) : TimeSpan.Zero) + timeDiff < sameCommentThreshold) { message = "Using same-comment threshold"; } else { message = "Splitting changeset due to same comment but exceeded threshold"; logger.WriteLine("NOTE: {0} ({1} second gap):", message, timeDiff.TotalSeconds); flush = true; } } else { //logger.WriteLine("NOTE: Splitting changeset due to different comment: {0} != {1}:", change.Revisions.Last.Value.Comment, revision.Comment); logger.WriteLine("NOTE: Splitting changeset due to different comment: {0} != {1}:", lastRevision.Comment ?? "null", revision.Comment ?? "null"); flush = true; } } } if (flush) { AddChangeset(change); flushedUsers.AddLast(user); } else if (user == pendingUser) { pendingChange = change; } } foreach (string user in flushedUsers) { pendingChangesByUser.Remove(user); } // if no pending change for user, create a new one if (pendingChange == null) { pendingChange = new Changeset(); pendingChange.User = pendingUser; pendingChangesByUser[pendingUser] = pendingChange; } // update the time of the change based on the last revision pendingChange.DateTime = revision.DateTime; // add the revision to the change pendingChange.Revisions.AddLast(revision); // track target files in changeset to detect conflicting actions if (!nonconflicting) { pendingChange.TargetFiles.Add(targetFile); } // build up a concatenation of unique revision comments var revComment = revision.Comment; if (revComment != null) { revComment = revComment.Trim(); if (revComment.Length > 0) { if (string.IsNullOrEmpty(pendingChange.Comment)) { pendingChange.Comment = revComment; } else if (!pendingChange.Comment.Contains(revComment)) { pendingChange.Comment += "\n" + revComment; } } } } } // flush all remaining changes foreach (var change in pendingChangesByUser.Values) { AddChangeset(change); } stopwatch.Stop(); logger.WriteSectionSeparator(); logger.WriteLine("Found {0} changesets in {1:HH:mm:ss}", changesets.Count, new DateTime(stopwatch.ElapsedTicks)); }); }
private bool IsCommentMissing(Revision r) { return(r.Comment == null); }
private bool HasSameComment(Revision rev1, Revision rev2) { return((rev1.Comment ?? "").Trim() == (rev2.Comment ?? "").Trim()); }
public void BuildChangesets() { workQueue.AddLast(delegate (object work) { logger.WriteSectionSeparator(); LogStatus(work, "Building changesets"); var unnamedLabelCount = 0ul; var stopwatch = Stopwatch.StartNew(); var pendingChangesByUser = new Dictionary<string, Changeset>(); foreach (var dateEntry in revisionAnalyzer.SortedRevisions) { var dateTime = dateEntry.Key; //foreach (Revision revision in dateEntry.Value) for (var node = dateEntry.Value.First; node != dateEntry.Value.Last.Next; node = node.Next) { var revision = node.Value; // VSS Destroyed Items: skip all. This will enable merging revisions separated by destroyed items. if (excludeAllDestroyedItems) { if ((revision.Action as VssNamedAction)?.Name?.PhysicalName != null && revisionAnalyzer.IsDestroyed((revision.Action as VssNamedAction).Name.PhysicalName) && !revisionAnalyzer.Database.ItemExists((revision.Action as VssNamedAction).Name.PhysicalName)) { continue; } } // VSS Adds: fill in comments for add from create action if (revision.Action.Type == VssActionType.Add && revision.Comment == null) node.Value = revision = new Revision(revision.DateTime, revision.User, revision.Item, revision.Version, FindCorrespondingAction(revision, VssActionType.Create)?.Comment, revision.Action); // VSS Labels: fill in empty label names as svn cannot have empty tag path. if (revision.Action.Type == VssActionType.Label && string.IsNullOrEmpty(((VssLabelAction)revision.Action).Label)) node.Value = revision = new Revision(revision.DateTime, revision.User, revision.Item, revision.Version, revision.Comment, new VssLabelAction(string.Format(unnamedLabelFormat, revision.DateTime, ++unnamedLabelCount))); // VSS Creates: skip all and exclude from changeset as it increases history complexity unnecessarily and the info is not useful at all. not used in exporter if (revision.Action.Type == VssActionType.Create) continue; // determine target of project revisions var actionType = revision.Action.Type; var namedAction = revision.Action as VssNamedAction; var targetFile = revision.Item.PhysicalName; if (namedAction != null) { targetFile = namedAction.Name.PhysicalName; } // Create actions are only used to obtain initial item comments; // items are actually created when added to a project var creating = (actionType == VssActionType.Create || (actionType == VssActionType.Branch && !revision.Item.IsProject)); // Share actions are never conflict (which is important, // since Share always precedes Branch) var nonconflicting = creating || (actionType == VssActionType.Share); // look up the pending change for user of this revision // and flush changes past time threshold var pendingUser = revision.User; Changeset pendingChange = null; LinkedList<string> flushedUsers = new LinkedList<string>(); foreach (var userEntry in pendingChangesByUser) { var user = userEntry.Key; var change = userEntry.Value; // flush change if file conflict or past time threshold var flush = false; var timeDiff = revision.DateTime - change.DateTime; if (user == pendingUser) { // VSS Label: make labels on their own changeset if ((revision.Action.Type == VssActionType.Label && change.Revisions.Last.Value.Action.Type != VssActionType.Label) || (revision.Action.Type != VssActionType.Label && change.Revisions.Last.Value.Action.Type == VssActionType.Label)) { logger.WriteLine("NOTE: Splitting changeset due to label: {0}", change.Revisions.Last.Value.Action); flush = true; } // Cannot combine due to file conflict. must be recorded as separate changes else if (!nonconflicting && change.TargetFiles.Contains(targetFile)) { logger.WriteLine("NOTE: Splitting changeset due to file conflict on {0}:", targetFile); flush = true; } } else { } /* if (!mergeAcrossDifferentUser && !HasSameUser(revision, change.Revisions.Last.Value)) { logger.WriteLine("NOTE: Splitting changeset due to different user: {0} != {1}", change.Revisions.Last.Value.User, revision.User); flush = true; } */ // additional check if not flushed above if (!flush) { if ((TimeSpan.Zero == anyCommentThreshold ? TimeSpan.FromSeconds(1) : TimeSpan.Zero) + timeDiff > anyCommentThreshold) { var lastRevision = FindLastRevisionWithNonEmptyComment(change); //if (HasSameComment(revision, change.Revisions.Last.Value)) if (HasSameComment(revision, lastRevision)) { string message; if ((TimeSpan.Zero == sameCommentThreshold ? TimeSpan.FromSeconds(1) : TimeSpan.Zero) + timeDiff < sameCommentThreshold) { message = "Using same-comment threshold"; } else { message = "Splitting changeset due to same comment but exceeded threshold"; logger.WriteLine("NOTE: {0} ({1} second gap):", message, timeDiff.TotalSeconds); flush = true; } } else { //logger.WriteLine("NOTE: Splitting changeset due to different comment: {0} != {1}:", change.Revisions.Last.Value.Comment, revision.Comment); logger.WriteLine("NOTE: Splitting changeset due to different comment: {0} != {1}:", lastRevision.Comment ?? "null", revision.Comment ?? "null"); flush = true; } } } if (flush) { AddChangeset(change); flushedUsers.AddLast(user); } else if (user == pendingUser) { pendingChange = change; } } foreach (string user in flushedUsers) pendingChangesByUser.Remove(user); // if no pending change for user, create a new one if (pendingChange == null) { pendingChange = new Changeset(); pendingChange.User = pendingUser; pendingChangesByUser[pendingUser] = pendingChange; } // update the time of the change based on the last revision pendingChange.DateTime = revision.DateTime; // add the revision to the change pendingChange.Revisions.AddLast(revision); // track target files in changeset to detect conflicting actions if (!nonconflicting) { pendingChange.TargetFiles.Add(targetFile); } // build up a concatenation of unique revision comments var revComment = revision.Comment; if (revComment != null) { revComment = revComment.Trim(); if (revComment.Length > 0) { if (string.IsNullOrEmpty(pendingChange.Comment)) { pendingChange.Comment = revComment; } else if (!pendingChange.Comment.Contains(revComment)) { pendingChange.Comment += "\n" + revComment; } } } } } // flush all remaining changes foreach (var change in pendingChangesByUser.Values) { AddChangeset(change); } stopwatch.Stop(); logger.WriteSectionSeparator(); logger.WriteLine("Found {0} changesets in {1:HH:mm:ss}", changesets.Count, new DateTime(stopwatch.ElapsedTicks)); }); }
private bool HasSameUser(Revision rev1, Revision rev2) { return((rev1.User ?? "").Trim() == (rev2.User ?? "").Trim()); }
private bool IsCommentMissing(Revision r) { return r.Comment == null; }
private bool HasSameUser(Revision rev1, Revision rev2) { return (rev1.User ?? "").Trim() == (rev2.User ?? "").Trim(); }
private bool HasSameComment(Revision rev1, Revision rev2) { return (rev1.Comment ?? "").Trim() == (rev2.Comment ?? "").Trim(); }
private Revision FindCorrespondingAction(Revision rev, VssActionType actionType) { if (rev.Action.Type != VssActionType.Edit && rev.Action.Type != VssActionType.Label) { var action = rev.Action as VssNamedAction; for (var node = revisionAnalyzer.SortedRevisions[rev.DateTime].Last; node != revisionAnalyzer.SortedRevisions[rev.DateTime].First.Previous; node = node.Previous) { var revision = node.Value; if (revision.Action.Type != VssActionType.Edit && revision.Action.Type != VssActionType.Label) { var targetaction = revision.Action as VssNamedAction; if (action.Name.PhysicalName == targetaction.Name.PhysicalName && targetaction.Type == actionType && revision.Comment != null) return revision; } } // look into sortedrevisions for the comment var foundPrevKey = false; var foundNextKey = false; var prevKey = (DateTime)DateTime.MinValue; var nextKey = (DateTime)DateTime.MinValue; foreach (var key in revisionAnalyzer.SortedRevisions.Keys) { if (key == rev.DateTime) { foundPrevKey = true; } else if (foundPrevKey) { foundNextKey = true; nextKey = key; break; } if (!foundPrevKey) prevKey = key; } if (foundPrevKey && rev.DateTime - prevKey < TimeSpan.FromMinutes(15)) for (var node = revisionAnalyzer.SortedRevisions[prevKey].Last; node != revisionAnalyzer.SortedRevisions[prevKey].First.Previous; node = node.Previous) { var revision = node.Value; if (revision.Action.Type != VssActionType.Edit && revision.Action.Type != VssActionType.Label) { var targetaction = revision.Action as VssNamedAction; if (action.Name.PhysicalName == targetaction.Name.PhysicalName && targetaction.Type == actionType && revision.Comment != null) return revision; } } if (foundNextKey && nextKey - rev.DateTime < TimeSpan.FromMinutes(15)) for (var node = revisionAnalyzer.SortedRevisions[nextKey].First; node != revisionAnalyzer.SortedRevisions[nextKey].Last.Next; node = node.Next) { var revision = node.Value; if (revision.Action.Type != VssActionType.Edit && revision.Action.Type != VssActionType.Label) { var targetaction = revision.Action as VssNamedAction; if (action.Name.PhysicalName == targetaction.Name.PhysicalName && targetaction.Type == actionType && revision.Comment != null) return revision; } } } return null; }
private bool ReplayRevision(VssPathMapper pathMapper, Revision revision, GitWrapper git, LinkedList <Revision> labels) { var needCommit = false; var actionType = revision.Action.Type; if (revision.Item.IsProject) { // note that project path (and therefore target path) can be // null if a project was moved and its original location was // subsequently destroyed var project = revision.Item; var projectName = project.LogicalName; var projectPath = pathMapper.GetProjectPath(project.PhysicalName); var projectDesc = projectPath; if (projectPath == null) { projectDesc = revision.Item.ToString(); logger.WriteLine("NOTE: {0} is currently unmapped", project); } VssItemName target = null; string targetPath = null; var namedAction = revision.Action as VssNamedAction; if (namedAction != null) { target = namedAction.Name; if (projectPath != null) { targetPath = Path.Combine(projectPath, target.LogicalName); } } bool isAddAction = false; bool writeProject = false; bool writeFile = false; VssItemInfo itemInfo = null; switch (actionType) { case VssActionType.Label: // defer tagging until after commit labels.AddLast(revision); break; case VssActionType.Create: // ignored; items are actually created when added to a project break; case VssActionType.Add: case VssActionType.Share: logger.WriteLine("{0}: {1} {2}", projectDesc, actionType, target.LogicalName); itemInfo = pathMapper.AddItem(project, target); isAddAction = true; break; case VssActionType.Recover: logger.WriteLine("{0}: {1} {2}", projectDesc, actionType, target.LogicalName); itemInfo = pathMapper.RecoverItem(project, target); isAddAction = true; break; case VssActionType.Delete: case VssActionType.Destroy: { logger.WriteLine("{0}: {1} {2}", projectDesc, actionType, target.LogicalName); itemInfo = pathMapper.DeleteItem(project, target); if (targetPath != null && !itemInfo.Destroyed) { if (target.IsProject) { if (Directory.Exists(targetPath)) { if (((VssProjectInfo)itemInfo).ContainsFiles()) { git.Remove(targetPath, true); needCommit = true; } else { // git doesn't care about directories with no files Directory.Delete(targetPath, true); } } } else { if (File.Exists(targetPath)) { // not sure how it can happen, but a project can evidently // contain another file with the same logical name, so check // that this is not the case before deleting the file if (pathMapper.ProjectContainsLogicalName(project, target)) { logger.WriteLine("NOTE: {0} contains another file named {1}; not deleting file", projectDesc, target.LogicalName); } else { File.Delete(targetPath); needCommit = true; } } } } } break; case VssActionType.Rename: { var renameAction = (VssRenameAction)revision.Action; logger.WriteLine("{0}: {1} {2} to {3}", projectDesc, actionType, renameAction.OriginalName, target.LogicalName); itemInfo = pathMapper.RenameItem(target); if (targetPath != null && !itemInfo.Destroyed) { var sourcePath = Path.Combine(projectPath, renameAction.OriginalName); if (target.IsProject ? Directory.Exists(sourcePath) : File.Exists(sourcePath)) { // renaming a file or a project that contains files? var projectInfo = itemInfo as VssProjectInfo; if (projectInfo == null || projectInfo.ContainsFiles()) { CaseSensitiveRename(sourcePath, targetPath, git.Move); needCommit = true; } else { // git doesn't care about directories with no files CaseSensitiveRename(sourcePath, targetPath, Directory.Move); } } else { logger.WriteLine("NOTE: Skipping rename because {0} does not exist", sourcePath); } } } break; case VssActionType.MoveFrom: // if both MoveFrom & MoveTo are present (e.g. // one of them has not been destroyed), only one // can succeed, so check that the source exists { var moveFromAction = (VssMoveFromAction)revision.Action; logger.WriteLine("{0}: Move from {1} to {2}", projectDesc, moveFromAction.OriginalProject, targetPath ?? target.LogicalName); var sourcePath = pathMapper.GetProjectPath(target.PhysicalName); var projectInfo = pathMapper.MoveProjectFrom( project, target, moveFromAction.OriginalProject); if (targetPath != null && !projectInfo.Destroyed) { if (sourcePath != null && Directory.Exists(sourcePath)) { if (projectInfo.ContainsFiles()) { git.Move(sourcePath, targetPath); needCommit = true; } else { // git doesn't care about directories with no files Directory.Move(sourcePath, targetPath); } } else { // project was moved from a now-destroyed project writeProject = true; } } } break; case VssActionType.MoveTo: { // handle actual moves in MoveFrom; this just does cleanup of destroyed projects var moveToAction = (VssMoveToAction)revision.Action; logger.WriteLine("{0}: Move to {1} from {2}", projectDesc, moveToAction.NewProject, targetPath ?? target.LogicalName); var projectInfo = pathMapper.MoveProjectTo( project, target, moveToAction.NewProject); if (projectInfo.Destroyed && targetPath != null && Directory.Exists(targetPath)) { // project was moved to a now-destroyed project; remove empty directory Directory.Delete(targetPath, true); } } break; case VssActionType.Pin: { var pinAction = (VssPinAction)revision.Action; if (pinAction.Pinned) { logger.WriteLine("{0}: Pin {1}", projectDesc, target.LogicalName); itemInfo = pathMapper.PinItem(project, target); } else { logger.WriteLine("{0}: Unpin {1}", projectDesc, target.LogicalName); itemInfo = pathMapper.UnpinItem(project, target); writeFile = !itemInfo.Destroyed; } } break; case VssActionType.Branch: { var branchAction = (VssBranchAction)revision.Action; logger.WriteLine("{0}: {1} {2}", projectDesc, actionType, target.LogicalName); itemInfo = pathMapper.BranchFile(project, target, branchAction.Source); // branching within the project might happen after branching of the file writeFile = true; } break; case VssActionType.Archive: // currently ignored { var archiveAction = (VssArchiveAction)revision.Action; logger.WriteLine("{0}: Archive {1} to {2} (ignored)", projectDesc, target.LogicalName, archiveAction.ArchivePath); } break; case VssActionType.Restore: { var restoreAction = (VssRestoreAction)revision.Action; logger.WriteLine("{0}: Restore {1} from archive {2}", projectDesc, target.LogicalName, restoreAction.ArchivePath); itemInfo = pathMapper.AddItem(project, target); isAddAction = true; } break; } if (targetPath != null) { if (isAddAction) { if (revisionAnalyzer.IsDestroyed(target.PhysicalName) && !database.ItemExists(target.PhysicalName)) { logger.WriteLine("NOTE: Skipping destroyed file: {0}", targetPath); itemInfo.Destroyed = true; } else if (target.IsProject) { Directory.CreateDirectory(targetPath); writeProject = true; } else { writeFile = true; } } if (writeProject && pathMapper.IsProjectRooted(target.PhysicalName)) { // create all contained subdirectories foreach (var projectInfo in pathMapper.GetAllProjects(target.PhysicalName)) { logger.WriteLine("{0}: Creating subdirectory {1}", projectDesc, projectInfo.LogicalName); Directory.CreateDirectory(projectInfo.GetPath()); } // write current rev of all contained files foreach (var fileInfo in pathMapper.GetAllFiles(target.PhysicalName)) { if (WriteRevision(pathMapper, actionType, fileInfo.PhysicalName, fileInfo.Version, target.PhysicalName, git)) { // one or more files were written needCommit = true; } } } else if (writeFile) { // write current rev to working path int version = pathMapper.GetFileVersion(target.PhysicalName); if (WriteRevisionTo(target.PhysicalName, version, targetPath)) { // add file explicitly, so it is visible to subsequent git operations git.Add(targetPath); needCommit = true; } } } } // item is a file, not a project else if (actionType == VssActionType.Edit || actionType == VssActionType.Branch) { // if the action is Branch, the following code is necessary only if the item // was branched from a file that is not part of the migration subset; it will // make sure we start with the correct revision instead of the first revision var target = revision.Item; // update current rev pathMapper.SetFileVersion(target, revision.Version); // write current rev to all sharing projects WriteRevision(pathMapper, actionType, target.PhysicalName, revision.Version, null, git); needCommit = true; } return(needCommit); }