IEnumerable<IChange> GetAllChanges(string fromCommit, GitBasedFileSystemSnapshot toSnapshot, string[] paths)
        {
            var currentCommit = toSnapshot.Commit;
            var currentSnapshot = toSnapshot;

            // empty path filter => result is empty
            if (paths != null && !paths.Any())
            {
                yield break;
            }

            // paths refers to file in the snapshot, we need to "translate" the paths to the paths in the git repository
            // assumes all paths are rooted
            var pathFilter = paths?.Select(p => GitBasedFileSystemSnapshot.SnapshotDirectoryName + p + FilePropertiesFile.FileNameSuffix).ToArray();
            
            while (currentCommit.Sha != fromCommit)
            {
                var parentCommit = GetParentSnapshotCommit(currentCommit);
                    
                // parent commit is initial commit
                if (parentCommit.Sha == m_Repository.GetInitialCommit().Sha)
                {

                    var treeChanges = m_Repository.Diff.Compare<TreeChanges>(parentCommit.Tree, currentCommit.Tree, pathFilter, null, new CompareOptions() { IncludeUnmodified = false });

                    // build changes
                    foreach (var change in GetChanges(treeChanges, null, currentSnapshot))
                    {
                        yield return change;
                    }

                    // there won't be any commit after this (we already reached the inital commit) 
                    // => abort the loop
                    break;
                }
                else
                {    
                    var parentSnapshot = GetSnapshot(parentCommit.Sha);

                    var treeChanges = m_Repository.Diff.Compare<TreeChanges>(parentCommit.Tree, currentCommit.Tree, pathFilter, null, new CompareOptions() { IncludeUnmodified = false });

                    // build changes
                    foreach(var change in GetChanges(treeChanges, parentSnapshot, currentSnapshot))
                    {
                        yield return change;
                    }

                    currentCommit = parentCommit;
                    currentSnapshot = parentSnapshot;
                }

            }
        }
        IEnumerable<IChange> GetChanges(TreeChanges treeChanges, GitBasedFileSystemSnapshot fromSnapshot, GitBasedFileSystemSnapshot toSnapshot)
        {
            foreach (var treeChange in treeChanges.Where(c => !IgnoreTreeChange(c)))
            {
                switch (treeChange.Status)
                {
                    case ChangeKind.Unmodified:
                        throw new InvalidOperationException("Unmodified changes should have been filtered out");

                    case ChangeKind.Modified:
                        var fromFile = fromSnapshot.GetFileForGitPath(treeChange.Path);
                        var toFile = toSnapshot.GetFileForGitPath(treeChange.Path);
                        yield return new Change(ChangeType.Modified, fromFile.ToReference(), toFile.ToReference());
                        break;                        

                    case ChangeKind.Added:
                        yield return new Change(ChangeType.Added, null, toSnapshot.GetFileForGitPath(treeChange.Path).ToReference());
                        break;

                    case ChangeKind.Deleted:
                        yield return new Change(ChangeType.Deleted, fromSnapshot.GetFileForGitPath(treeChange.Path).ToReference(), null);
                        break;

                    default:
                        throw new NotImplementedException();
                }

            }
        }
 IEnumerable<ChangeList> GetChangeLists(string fromCommit, GitBasedFileSystemSnapshot toSnapshot, string[] paths)
 {
     var changeLists = GetAllChanges(fromCommit, toSnapshot, paths)
         .GroupBy(change => change.Path, StringComparer.InvariantCultureIgnoreCase)
         .Select(group => new ChangeList(group.Reverse()));
         
     return changeLists;
 }