Merging The History of Two Documents in SharePoint 2010

The problem I needed to solve was this: Two users were independently creating the same document. Once this was discovered, one of them was chosen to be the master document. But the users didn’t want to lose the other document, even though it was not chosen as the master. The users wanted to merge this document into the history of the master document. Technically, it is not possible to alter the history of any document in SharePoint. So I decided we would create a new third document, which was generated based on the two documents to be merged. By using the SharePoint API I could control the dates and merge the histories of the two documents into a third one. Once that was done, the original documents could be deleted.

The basic algorithm works like this:

  1. Choose 2 documents, one to be the master, one to merge into the master
  2. Fetch the history of both items into a List<>
  3. Sort the list by date
  4. Loop through the list and write the historical files to a new list item, creating a new history
  5. Write the current version of the merge document  to the new list item
  6. Write the current version of the master document to the new list item, including the properties

I did try to keep the properties to all the previous versions, but there was some resulting weirdness that went away when that part was removed. Since the metadata wasn’t a strict requirement, but a nice-to-have, I didn’t investigate that any further. YMMV. Here is the code for the algorithm:

/// <summary>
/// Merges the history of two SPListItems into a single SPListItem
/// </summary>
/// <param name="siteUrl">The URL of the SharePoint site</param>
/// <param name="listName">The Name of a list containing the documents to be merged</param>
/// <param name="keepDocName">The name of the document which is the "Master"</param>
/// <param name="mergeDocName">The name of the document which is merging it's history into the master.</param>
/// <param name="newName">The name of a third document which is the destination of the other two</param>
protected void MergeDocuments(string siteUrl, string listName, string keepDocName, string mergeDocName, string newName)
{
    
//Attach to SharePoint, get the list and documents
     var site = new SPSite(siteUrl);
     var web = site.OpenWeb();
     var list = web.Lists[listName];
     var keepUrl = string.Format("{0}/{1}", list.RootFolder.ServerRelativeUrl, keepDocName);
     var discardUrl = string.Format("{0}/{1}", list.RootFolder.ServerRelativeUrl, mergeDocName);
     var keep = web.GetListItem(keepUrl);
     var merge = web.GetListItem(discardUrl);
     //Push the versions from each document into a list that can be sorted by date
     var allVersions = keep.Versions.Cast<SPListItemVersion>().ToList();
     allVersions.AddRange(merge.Versions.Cast<SPListItemVersion>());
     allVersions.Sort(new SortVersionsByDate()); //SortVersionsByDate is a simple
IComparer for the created date
     //Create the upload url for the new document being merged
     var newUrl = string.Format("{0}/{1}", list.RootFolder.ServerRelativeUrl, newName);
     //Write each old version as the new file to create a merged history
     foreach (var version in allVersions)
     {
          if (version.IsCurrentVersion)
                continue; //Skip the current versions of each, we need for them to be the last two docs
          var oldFile = version.ListItem.File.Versions.GetVersionFromID(version.VersionId);
          var oldestFile = list.RootFolder.Files.Add(newUrl, oldFile.OpenBinaryStream(),
                                                     null, version.CreatedBy.User,
                                                     version.CreatedBy.User, version.Created, version.Created,
                                                     oldFile.CheckInComment, true);
          //Even though the dates were set in the call above, it doesn't work
         
// when setting them in the past, so call this fixes the issue
          UpdateListItem(oldestFile.Item, version.Created, version.Created);
          list.Update();
      }
      //Add the last version of the merged document
      WriteFileToSharePoint(list, merge, newUrl, false);
      //Add the Final version of the document
      WriteFileToSharePoint(list, keep, newUrl, true);
}
/// <summary>
///
Helper function to set the created and modified dates on a list item.
///
The reason for this function is to allow the dates to be set in the past.
/// 
</summary>
/// <param name="item">a SPListItem
</param>
/// <param name="created">Date created
</param>
/// <param name="modified">Date modified
</param>
protected void UpdateListItem(SPListItem item, DateTime created, DateTime modified)
{
     
//Update the modification/creation dates.
      item[SPBuiltInFieldId.Modified] = modified.ToLocalTime();
      item[SPBuiltInFieldId.Created] = created.ToLocalTime();
      item.UpdateOverwriteVersion(); //Keep the changes to the date/time from making a new version of the item
}
/// <summary>
///
Upload a file to a list based on an existing SPListItem
/// 
</summary>
/// <param name="list">The target list for the upload
</param>
/// <param name="doc">The existing SPListItem
</param>
/// <param name="newUrl">The url to upload the file to
</param>
/// 
<param name="final"></param>
protected void WriteFileToSharePoint(SPList list, SPListItem doc, string newUrl, bool final)
{
     Hashtable props = null;
     if (final)
        props = doc.Properties;
     var lastMergedFile = list.RootFolder.Files.Add(newUrl, doc.File.OpenBinaryStream(), props, doc.File.Author,
                          doc.File.Author, doc.File.TimeCreated, doc.File.TimeCreated,
                          doc.File.CheckInComment, true);
     UpdateListItem(lastMergedFile.Item, doc.File.TimeCreated, doc.File.TimeLastModified);
     list.Update();
}

I deployed this as a PowerShell cmdlet, which is explained here and here. Essentially that route was chosen because the merging was to be performed on request by an administrator. It would be easy enough to wire this up to a ribbon command in the UI though.