Exemple #1
0
        private ChangeGroup createChangeGroupForSnapshot(int snapShotChangeset, long executionOrder)
        {
            ChangeGroup group = m_changeGroupService.CreateChangeGroupForDeltaTable(snapShotChangeset.ToString());

            group.Owner          = null;
            group.Comment        = string.Format("Initial Check-in as snapshot at changeset {0}", snapShotChangeset);
            group.ChangeTimeUtc  = DateTime.UtcNow;
            group.Status         = ChangeStatus.Delta;
            group.ExecutionOrder = executionOrder;
            return(group);
        }
Exemple #2
0
        /// <summary>
        /// Creates the change group.
        /// </summary>
        /// <param name="changeset">The changeset.</param>
        /// <param name="executionOrder">The execution order.</param>
        /// <returns></returns>
        private ChangeGroup CreateChangeGroup(int changeset, long executionOrder)
        {
            ChangeGroup group = changeGroupService.CreateChangeGroupForDeltaTable(changeset.ToString(CultureInfo.CurrentCulture));

            group.Owner          = null;
            group.Comment        = string.Format(CultureInfo.CurrentCulture, "Changeset {0}", changeset);
            group.ChangeTimeUtc  = DateTime.UtcNow;
            group.Status         = ChangeStatus.Delta;
            group.ExecutionOrder = executionOrder;
            return(group);
        }
Exemple #3
0
        private ChangeGroup createChangeGroupForSnapshot(DateTime snapShotTime, long executionOrder)
        {
            ChangeGroup group = m_changeGroupService.CreateChangeGroupForDeltaTable(snapShotTime.ToString());

            group.Owner   = null;
            group.Comment = string.Format("Initial Check-in as snapshot at changeset {0}", snapShotTime);
            // ChangeTimeUtc should be set to DateTime.MaxValue since we don't know the actual original time the change was made on the source
            group.ChangeTimeUtc  = DateTime.MaxValue;
            group.Status         = ChangeStatus.Delta;
            group.ExecutionOrder = executionOrder;
            return(group);
        }
Exemple #4
0
        internal ChangeGroup GetChangeGroupForLatestFieldValues(ChangeGroupService changeGroupService)
        {
            string author = (string)WorkItem.Fields[CoreField.ChangedBy].Value;

            if (string.IsNullOrEmpty(author))
            {
                author = (string)WorkItem.Fields[CoreField.AuthorizedAs].Value;
            }
            DateTime changedDate = (DateTime)WorkItem.Fields[CoreField.ChangedDate].Value;

            List <Field> fieldsForSync = new List <Field>();

            foreach (Field f in WorkItem.Fields)
            {
                // filter out System.History with empty new value
                if (TFStringComparer.FieldName.Equals(f.ReferenceName, CoreFieldReferenceNames.History))
                {
                    if (string.IsNullOrEmpty(f.Value as string))
                    {
                        continue;
                    }
                }

                if (MustTakeField(f.FieldDefinition))
                {
                    fieldsForSync.Add(f);
                }
            }

            if (fieldsForSync.Count == 0)
            {
                return(null);
            }

            XmlDocument migrationActionDetails = CreateFieldRevisionDescriptionDoc(
                WorkItem.Rev, author, changedDate, fieldsForSync, new List <Field>());

            ChangeGroup changeGroup = changeGroupService.CreateChangeGroupForDeltaTable(
                string.Format("{0}:{1}", WorkItem.Id, WorkItem.Rev));
            IMigrationAction action = changeGroup.CreateAction(
                // Always generate Edit even for rev 1 in force sync case
                // Action will be changed to Add later if history not found for rev 1
                WellKnownChangeActionId.Edit,
                new TfsWITMigrationItem(WorkItem, WorkItem.Rev),
                WorkItem.Id.ToString(),
                "",
                WorkItem.Rev.ToString(),
                "",
                WellKnownContentType.WorkItem.ReferenceName,
                migrationActionDetails);

            return(changeGroup);
        }
        internal ChangeGroup CreateChangeGroup(
            ChangeGroupService changeGroupService,
            ClearQuestMigrationContext migrationContext,
            bool isLastRevOfThisSyncCycle)
        {
            ChangeGroup changeGroup = changeGroupService.CreateChangeGroupForDeltaTable(ChangeGroupName);
            OAdEntity   record      = CQWrapper.GetEntity(CQSession, EntityDefName, EntityDispName);

            if (IsRecordHistory)
            {
                XmlDocument recordHistDesc = CreateRecordHistoryDesc(record, HistoryValue, HistoryFieldName + "::" + Version, isLastRevOfThisSyncCycle);

                changeGroup.CreateAction(WellKnownChangeActionId.Edit,
                                         this,
                                         MigrationRecordId,
                                         "",
                                         Version,
                                         "",
                                         WellKnownContentType.WorkItem.ReferenceName,
                                         recordHistDesc);
            }
            else
            {
                XmlDocument recordDesc = CreateRecordDesc(record, Version, migrationContext, isLastRevOfThisSyncCycle);

                if (Version.Equals(NewRecordVersion, StringComparison.InvariantCulture))
                {
                    changeGroup.CreateAction(WellKnownChangeActionId.Add,
                                             this,
                                             MigrationRecordId,
                                             "",
                                             Version,
                                             "",
                                             WellKnownContentType.WorkItem.ReferenceName,
                                             recordDesc);
                }
                else
                {
                    changeGroup.CreateAction(WellKnownChangeActionId.Edit,
                                             this,
                                             MigrationRecordId,
                                             "",
                                             Version,
                                             "",
                                             WellKnownContentType.WorkItem.ReferenceName,
                                             recordDesc);
                }
            }


            return(changeGroup);
        }
        /// <summary>
        /// Analyzes the TFS changeset to generate a change group.
        /// </summary>
        /// <param name="logRecord"></param>
        /// <returns></returns>
        private int analyzeChangeset(ChangeSet changeSet, int[] mappedChanges)
        {
            if (changeSet == null)
            {
                throw new ArgumentNullException("changeSet");
            }

            lazyInit();

            TraceManager.TraceInformation("Starting analysis of Subversion revision {0}", changeSet.Revision);

            int         changeCount = 0;
            ChangeGroup group       = m_changeGroupService.CreateChangeGroupForDeltaTable(changeSet.Revision.ToString(CultureInfo.InvariantCulture));

            populateChangeGroupMetaData(group, changeSet);
            if (changeSet != null)
            {
                m_algorithm.CurrentChangeset = changeSet;
                foreach (Change change in changeSet.Changes)
                {
                    // Either no snapshot start point is specified or we already passed the snapshot start point.
                    if (IsPathMapped(change.FullServerPath))
                    {
                        m_algorithm.Execute(change, group);
                    }
                    else
                    {
                        m_algorithm.ExecuteNonMapped(change, group, mappedChanges);
                    }
                }
                m_algorithm.Finish(group);
            }

            changeCount = group.Actions.Count;

            if (group.Actions.Count > 0)
            {
                group.Save();
            }

            if (changeCount == 0)
            {
                TraceManager.TraceInformation("No relevent changes found in SVN revision {0}", changeSet.Revision);
            }
            return(changeCount);
        }
Exemple #7
0
        internal ChangeGroup GetChangeGroupForLatestAttachments(ChangeGroupService changeGroupService)
        {
            List <TfsMigrationFileAttachment> files = new List <TfsMigrationFileAttachment>();

            if (WorkItem.Attachments.Count == 0)
            {
                return(null);
            }
            else
            {
                foreach (Attachment attachment in WorkItem.Attachments)
                {
                    files.Add(new TfsMigrationFileAttachment(attachment, m_core.TfsTPC.Uri.AbsoluteUri));
                }

                Guid        changeActionId = WellKnownChangeActionId.AddAttachment;
                ChangeGroup changeGroup    = changeGroupService.CreateChangeGroupForDeltaTable(
                    string.Format("{0}:{1}", WorkItem.Id, "Attachments"));
                foreach (TfsMigrationFileAttachment attachmentFile in files)
                {
                    XmlDocument migrationActionDetails = CreateAttachmentDescriptionDoc(attachmentFile, WorkItem.Rev.ToString());
                    changeGroup.CreateAction(
                        changeActionId,
                        attachmentFile,
                        WorkItem.Id.ToString(),
                        "",
                        "0",
                        "",
                        WellKnownContentType.WorkItem.ReferenceName,
                        migrationActionDetails);
                    TraceManager.TraceVerbose(String.Format("Generating AddAttachment change action: Work Item: {0}, Attachment File: {1}",
                                                            WorkItem.Id.ToString(), attachmentFile.Name));
                }

                return(changeGroup);
            }
        }
Exemple #8
0
        /// <summary>
        /// Generate the delta table
        /// </summary>
        public void GenerateDeltaTable()
        {
            // Load the current high water mark from any previous run of this session
            m_hwmDelta.Reload();

            m_renameList        = null;
            m_renameListUpdated = false;

            // Removing the in-progress change groups created by this side (fils system)
            m_changeGroupService.RemoveInProgressChangeGroups();

            if (m_changeGroupService.GetInProgressMigrationInstructionCount() > 0)
            {
                // If there are in-progress migration instructions translated from the other side, mark this delta table as contentcon flict detection only.
                m_contentConflictDetectionOnly = true;
                TraceManager.TraceInformation("Migration instruction in progress, the delta table will be generated for content conflict detection only");
            }
            else
            {
                m_contentConflictDetectionOnly = false;
            }

            DateTime newHighWaterMarkTime = DateTime.Now;

            List <String> pathsToBeVerified = new List <String>();
            int           versionToBeSynced = 0;

            // In a two-way sync, versionToBeSynced is set to the last sync point - either the latest changeset migrated from TFS or the latest changeset migrated to TFS
            // In one way sync, versionToBeSynced is always set to the GetLatestChangesetId of TFS
            if (m_hwmLastSyncedTfsChangeset != null)
            {
                // m_hwmLastSyncedTfsChangeset is the latest TFS changeset migrated to the file system side.
                // This highwater mark will be set when TfsFileSystemAnalysisProvider is combined with another provider to form a two-way sync.
                m_hwmLastSyncedTfsChangeset.Reload();
                // m_lastHighWaterMarkMigratedToPeer is the latest TFS changeset migrated from this TfsFileSystemAnalysisProvider.
                m_lastHighWaterMarkMigratedToPeer.Reload();
                versionToBeSynced = m_lastHighWaterMarkMigratedToPeer.Value > m_hwmLastSyncedTfsChangeset.Value ?
                                    m_lastHighWaterMarkMigratedToPeer.Value : m_hwmLastSyncedTfsChangeset.Value;
            }
            else
            {
                versionToBeSynced = m_destinationTfs.GetLatestChangesetId();
            }

            FileSystemVerifier fileSystemVerifier = new FileSystemVerifier();

            ChangeGroup changeGroup = m_changeGroupService.CreateChangeGroupForDeltaTable(DateTime.Now.ToString());

            populateChangeGroupMetaData(changeGroup);

            foreach (MappingEntry m in ConfigurationService.Filters)
            {
                if (m.Cloak)
                {
                    continue;
                }
                string canonicalPath = removeTrailingSlash(m.Path);
                fileSystemVerifier.AddPathForVerification(canonicalPath);
                analyzeFolder(canonicalPath, versionToBeSynced, changeGroup);
            }

            if (!fileSystemVerifier.Verify())
            {
                TraceManager.TraceError(
                    "Analysis failed as the local file system state was changed during the analysis phase. No changes were created.");
                markChangeGroupAsObsolete(changeGroup);
                return;
            }

            if (changeGroup.Actions.Count > 0)
            {
                // We only want to promote deltas to pending if we actually created and saved a change group.
                changeGroup.Save();
                m_changeGroupService.PromoteDeltaToPending();
            }
            else
            {
                markChangeGroupAsObsolete(changeGroup);
            }

            m_lastTfsChangesetAnalyzed.Update(versionToBeSynced);
            m_hwmDelta.Update(newHighWaterMarkTime);
            TraceManager.TraceInformation(String.Format(CultureInfo.InvariantCulture,
                                                        TfsFileSystemResources.UpdatedHighWaterMark, newHighWaterMarkTime));
        }
Exemple #9
0
        private void ComputeDeltaPerRecord(
            OAdEntity aRecord)
        {
            OAdEntityDef aEntityDef       = CQWrapper.GetEntityDef(m_userSession, CQWrapper.GetEntityDefName(aRecord));
            string       recordDispName   = CQWrapper.GetEntityDisplayName(aRecord);
            string       recordEntDefName = CQWrapper.GetEntityDefName(aRecord);

            #region process history

            bool recordContentIsModified = false;
            bool maybeNewRecord          = false;
            Dictionary <string, List <ClearQuestRecordItem> > historyDelta = new Dictionary <string, List <ClearQuestRecordItem> >();
            Dictionary <string, int> perHistoryFieldLastIndex = new Dictionary <string, int>();   // needed for updating processed delta

            // find all history fields
            OAdHistoryFields aHistFields = CQWrapper.GetHistoryFields(aRecord);
            int  historyFldCount         = CQWrapper.HistoryFieldsCount(aHistFields);
            bool containsNewHistory      = false;
            for (int histFldIndex = 0; histFldIndex < historyFldCount; histFldIndex++)
            {
                object          ob               = (object)histFldIndex;
                OAdHistoryField aHistoryField    = CQWrapper.HistoryFieldsItem(aHistFields, ref ob);
                string          historyFieldName = CQWrapper.GetHistoryFieldName(aHistoryField);

                // find last processed history entry for this history field
                string lookupItemId   = CQDeltaComputationProgressLookupService.CreateHistoryItemId(recordEntDefName, recordDispName, historyFieldName);
                int    startHistIndex = 1 + DeltaComputeProgressService.GetLastProcessedItemVersion(lookupItemId);

                // find all history in a particular history field
                int historyCount = CQWrapper.HistoryFieldHistoriesCount(aHistoryField);
                for (int histIndex = startHistIndex; histIndex < historyCount; histIndex++)
                {
                    object     obHistIndex = (object)histIndex;
                    OAdHistory aHistory    = CQWrapper.HistoryFieldHistoriesItem(aHistoryField, ref obHistIndex);
                    CQHistory  cqHistory   = new CQHistory(aHistory);

                    CQMigrationItem migrationItem = new CQHistoryMigrationItem(recordDispName, historyFieldName, histIndex);

                    if (TranslationService.IsSyncGeneratedItemVersion(ClearQuestRecordItem.GetMigrationRecordId(recordEntDefName, recordDispName),
                                                                      migrationItem.MigrationItemVersion,
                                                                      m_configurationService.SourceId))
                    {
                        continue;
                    }

                    if (histIndex == 0)
                    {
                        maybeNewRecord = true;
                    }

                    // add unprocessed history fields for processing
                    if (!historyDelta.ContainsKey(historyFieldName))
                    {
                        historyDelta.Add(historyFieldName, new List <ClearQuestRecordItem>(historyCount));
                    }
                    historyDelta[aHistoryField.fieldname].Add(new ClearQuestRecordItem(aRecord, aHistory, historyFieldName, histIndex.ToString()));
                    containsNewHistory = true;

                    // based on action type, we decide whether content change is needed
                    int actionType = CQWrapper.GetActionDefType(aEntityDef, cqHistory.Action);
                    switch (actionType)
                    {
                    case CQConstants.ACTION_SUBMIT:
                        break;

                    case CQConstants.ACTION_MODIFY:
                        recordContentIsModified = true;
                        break;

                    case CQConstants.ACTION_CHANGE:
                        break;

                    case CQConstants.ACTION_DUPLICATE:
                        break;

                    case CQConstants.ACTION_UNDUPLICATE:
                        break;

                    case CQConstants.ACTION_IMPORT:
                        break;

                    case CQConstants.ACTION_DELETE:
                        TraceManager.TraceInformation(ClearQuestResource.ClearQuest_Msg_RecordDeleted, recordEntDefName, recordDispName);
                        break;

                    case CQConstants.ACTION_BASE:
                        break;

                    case CQConstants.ACTION_RECORD_SCRIPT_ALIAS:
                        break;
                    }
                }

                perHistoryFieldLastIndex.Add(historyFieldName, historyCount - 1);
            }

            #endregion

            #region generate delta for content and history

            if (maybeNewRecord || recordContentIsModified)
            {
                // the first revision, i.e. "Submit", of a CQ record is always hard-coded to be '1'
                CQMigrationItem contentMigrationAction = new CQMigrationItem(recordDispName, ClearQuestRecordItem.NewRecordVersion);
                bool            isNewRecord            = false;
                if (maybeNewRecord)
                {
                    isNewRecord = !(DeltaComputeProgressService.IsMigrationItemProcessed(recordDispName, ClearQuestRecordItem.NewRecordVersionValue));
                }

                if (!isNewRecord)
                {
                    // all subsequent record "MODIFICATIONs" are hard-coded to be "update@<Now.Ticks>"
                    contentMigrationAction.MigrationItemVersion = ClearQuestRecordItem.RecordUpdateVersion + "@" + DateTime.Now.Ticks;
                }

                ClearQuestRecordItem recordContentItem = new ClearQuestRecordItem(aRecord, contentMigrationAction.MigrationItemVersion);
                recordContentItem.CQSession = m_userSession;
                recordContentItem.Version   = contentMigrationAction.MigrationItemVersion;
                ChangeGroup contentChangeGroup = recordContentItem.CreateChangeGroup(
                    m_changeGroupService, m_migrationContext, isNewRecord && m_isLastRevisionAutoCorrectionEnabled);
                contentChangeGroup.Save();

                if (isNewRecord && !containsNewHistory)
                {
                    DeltaComputeProgressService.UpdateCache(recordDispName, ClearQuestRecordItem.NewRecordVersionValue);
                }
            }

            var lastHistoryRecordItem = historyDelta[historyDelta.Keys.Last()].Last();
            foreach (string histFieldName in historyDelta.Keys)
            {
                foreach (ClearQuestRecordItem recordHistItem in historyDelta[histFieldName])
                {
                    recordHistItem.CQSession = m_userSession;
                    ChangeGroup changeGroup = recordHistItem.CreateChangeGroup(
                        m_changeGroupService,
                        m_migrationContext,
                        (CQStringComparer.FieldName.Equals(recordHistItem.HistoryFieldName, lastHistoryRecordItem.HistoryFieldName) &&
                         recordHistItem.Version.Equals(lastHistoryRecordItem.Version, StringComparison.OrdinalIgnoreCase) &&
                         m_isLastRevisionAutoCorrectionEnabled));
                    changeGroup.Save();
                }

                Debug.Assert(perHistoryFieldLastIndex.ContainsKey(histFieldName), "perHistoryFieldLastIndex.ContainsKey(histFieldName) returns false");
                string deltaComputeProcessLookupId = CQDeltaComputationProgressLookupService.CreateHistoryItemId(recordEntDefName, recordDispName, histFieldName);
                DeltaComputeProgressService.UpdateCache(deltaComputeProcessLookupId, perHistoryFieldLastIndex[histFieldName]);
            }

            #endregion

            #region process attachment

            OAdAttachmentFields aAttachmentFields = CQWrapper.GetAttachmentFields(aRecord);

            for (int aAttachmentFieldsIndex = 0;
                 aAttachmentFieldsIndex < CQWrapper.AttachmentsFieldsCount(aAttachmentFields);
                 aAttachmentFieldsIndex++)
            {
                object             ob = (object)aAttachmentFieldsIndex;
                OAdAttachmentField aAttachmentField = CQWrapper.AttachmentsFieldsItem(aAttachmentFields, ref ob);
                string             fieldName        = CQWrapper.GetAttachmentFieldName(aAttachmentField);

                ChangeGroup changeGroup = m_changeGroupService.CreateChangeGroupForDeltaTable(
                    string.Format("{0}:{1}:{2}", recordDispName, "Attachments", fieldName));

                // process all attachments
                OAdAttachments attachments = CQWrapper.GetAttachments(aAttachmentField);
                for (int attachmentIndex = 0;
                     attachmentIndex < CQWrapper.AttachmentsCount(attachments);
                     attachmentIndex++)
                {
                    object        obIndex     = (object)attachmentIndex;
                    OAdAttachment aAttachment = CQWrapper.AttachmentsItem(attachments, ref obIndex);

                    ClearQuestAttachmentItem attachmentItem = new ClearQuestAttachmentItem(aRecord, aAttachmentField, aAttachment, UserSessionConnConfig);
                    attachmentItem.CQSession = m_userSession;
                    attachmentItem.CreateChangeAction(changeGroup, lastHistoryRecordItem.Version);
                }

                if (changeGroup.Actions.Count > 0)
                {
                    changeGroup.Save();
                }
            }

            #endregion
        }
Exemple #10
0
        internal void ComputeFieldDelta(
            ChangeGroupService changeGroupService,
            DateTime waterMarkChangeStartTime,
            FieldValueComparer tfsValueComparer,
            ITranslationService translationService,
            ConfigurationService configService,
            List <ChangeGroup> groups,
            IsWorkItemRevisionProcessed processedRevCallBack)
        {
            Guid sourceId = configService.SourceId;

            List <Revision> revsToBeSynced = FindUnsyncedRevisions(waterMarkChangeStartTime,
                                                                   translationService,
                                                                   sourceId,
                                                                   processedRevCallBack);

            Dictionary <int, object> fieldValueBaseline = new Dictionary <int, object>();

            TryEstablishFieldValueBaseline(fieldValueBaseline, revsToBeSynced);

            foreach (Revision rev in revsToBeSynced)
            {
                // get basic revision info: revision#, author#, and change time
                int    revIndex = (int)rev.Fields[CoreField.Rev].Value;
                string author   = (string)rev.Fields[CoreField.ChangedBy].Value;
                if (string.IsNullOrEmpty(author))
                {
                    author = (string)rev.Fields[CoreField.AuthorizedAs].Value;
                }
                DateTime changedDate = (DateTime)rev.Fields[CoreField.ChangedDate].Value;

                List <Field> fieldForSync   = new List <Field>();
                List <Field> skippingFields = new List <Field>();
                foreach (Field f in rev.Fields)
                {
                    // filter out System.History with empty new value
                    if (TFStringComparer.FieldName.Equals(f.ReferenceName, CoreFieldReferenceNames.History))
                    {
                        if (string.IsNullOrEmpty(f.Value as string))
                        {
                            continue;
                        }
                    }

                    if (MustTakeField(f.FieldDefinition))
                    {
                        // find out fields that changed in this revision
                        object oldValue;
                        fieldValueBaseline.TryGetValue(f.Id, out oldValue);
                        //***Note: When it is a new work item (unmatched on the other side),
                        //         we need to always include fields
                        if ((null != f.Value && (!tfsValueComparer.Equals(oldValue, f.Value) || revIndex == 1)) ||
                            (null == f.Value && null != oldValue && revIndex != 1))
                        {
                            fieldForSync.Add(f);
                            fieldValueBaseline[f.Id] = f.Value;
                        }
                        else if (IsReferencedField(f, configService))
                        {
                            skippingFields.Add(f);
                        }
                    }
                    else if (IsReferencedField(f, configService))
                    {
                        skippingFields.Add(f);
                    }
                }

                if (fieldForSync.Count == 0)
                {
                    continue;
                }

                XmlDocument migrationActionDetails = CreateFieldRevisionDescriptionDoc(
                    revIndex, author, changedDate, fieldForSync, skippingFields);

                Guid changeActionId = revIndex == 1 ?
                                      WellKnownChangeActionId.Add : WellKnownChangeActionId.Edit;

                /// TODO: consider batching revs of different workitems in one change group
                ChangeGroup changeGroup = changeGroupService.CreateChangeGroupForDeltaTable(
                    string.Format("{0}:{1}", WorkItem.Id, revIndex));
                IMigrationAction action = changeGroup.CreateAction(
                    changeActionId,
                    new TfsWITMigrationItem(WorkItem, revIndex),
                    WorkItem.Id.ToString(),
                    "",
                    revIndex.ToString(),
                    "",
                    WellKnownContentType.WorkItem.ReferenceName,
                    migrationActionDetails);

                groups.Add(changeGroup);
            }
        }
Exemple #11
0
        internal void ComputeAttachmentDelta(
            ChangeGroupService changeGroupService,
            DateTime waterMarkChangeStartTime,
            ITranslationService translationService,
            Guid sourceId,
            List <ChangeGroup> groups)
        {
            List <TfsMigrationFileAttachment> files = new List <TfsMigrationFileAttachment>();

            if (WorkItem.Attachments.Count > 0)
            {
                List <Revision> revsToBeSynced = FindUnsyncedRevisions(waterMarkChangeStartTime,
                                                                       translationService,
                                                                       sourceId,
                                                                       null);

                bool hasAttachmentChanges = false;
                foreach (Revision rev in revsToBeSynced)
                {
                    if (rev.Index == 0 &&
                        (int)rev.Fields[CoreField.AttachedFileCount].Value > 0)
                    {
                        hasAttachmentChanges = true;
                        break;
                    }

                    if (rev.Index > 0)
                    {
                        int      currAttchCount = (int)rev.Fields[CoreField.AttachedFileCount].Value;
                        Revision prevRev        = rev.WorkItem.Revisions[rev.Index - 1];
                        int      prevAttchCount = (int)prevRev.Fields[CoreField.AttachedFileCount].Value;
                        if (currAttchCount != prevAttchCount)
                        {
                            hasAttachmentChanges = true;
                            break;
                        }
                    }
                }

                if (!hasAttachmentChanges)
                {
                    return;
                }

                foreach (Attachment attachment in WorkItem.Attachments)
                {
                    if (attachment.AttachedTimeUtc <= waterMarkChangeStartTime ||
                        !attachment.IsSaved)
                    {
                        continue;
                    }
                    files.Add(new TfsMigrationFileAttachment(attachment));
                }
            }

            Guid        changeActionId = WellKnownChangeActionId.AddAttachment;
            ChangeGroup changeGroup    = changeGroupService.CreateChangeGroupForDeltaTable(
                string.Format("{0}:{1}", WorkItem.Id, "Attachments"));

            foreach (TfsMigrationFileAttachment attachmentFile in files)
            {
                XmlDocument migrationActionDetails = CreateAttachmentDescriptionDoc(attachmentFile, WorkItem.Rev.ToString());
                changeGroup.CreateAction(
                    changeActionId,
                    attachmentFile,
                    WorkItem.Id.ToString(),
                    "",
                    "0",
                    "",
                    WellKnownContentType.WorkItem.ReferenceName,
                    migrationActionDetails);
            }

            // VERY IMPORTANT: use the RelatedArtifactsStore to detect detailed attachment changes
            WorkItemAttachmentStore       store = new WorkItemAttachmentStore(sourceId);
            List <FileAttachmentMetadata> additionalAttachmentToDelete;

            store.UpdatePerItemAttachmentChangesByCheckingRelatedItemRecords(
                WorkItem.Id.ToString(), changeGroup, out additionalAttachmentToDelete);

            foreach (FileAttachmentMetadata attch in additionalAttachmentToDelete)
            {
                TfsMigrationFileAttachment attachmentFile = new TfsMigrationFileAttachment(attch);
                XmlDocument migrationActionDetails        = CreateAttachmentDescriptionDoc(attachmentFile, WorkItem.Rev.ToString());
                changeGroup.CreateAction(
                    WellKnownChangeActionId.DelAttachment,
                    attachmentFile,
                    WorkItem.Id.ToString(),
                    "",
                    "0",
                    "",
                    WellKnownContentType.WorkItem.ReferenceName,
                    migrationActionDetails);
            }

            groups.Add(changeGroup);
        }