{"action":"created","comment":{"url":"https://api.github.com/repos/saros-project/saros/pulls/comments/233893516","pull_request_review_id":175412825,"id":233893516,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIzMzg5MzUxNg==","diff_hunk":"@@ -1,529 +1,870 @@\n package de.fu_berlin.inf.dpp.intellij.project;\n \n+import com.intellij.openapi.editor.Document;\n+import com.intellij.openapi.editor.Editor;\n+import com.intellij.openapi.module.Module;\n+import com.intellij.openapi.module.ModuleUtil;\n+import com.intellij.openapi.project.Project;\n+import com.intellij.openapi.roots.ContentIterator;\n+import com.intellij.openapi.vfs.VfsUtilCore;\n import com.intellij.openapi.vfs.VirtualFile;\n import com.intellij.openapi.vfs.VirtualFileCopyEvent;\n import com.intellij.openapi.vfs.VirtualFileEvent;\n+import com.intellij.openapi.vfs.VirtualFileFilter;\n import com.intellij.openapi.vfs.VirtualFileListener;\n import com.intellij.openapi.vfs.VirtualFileMoveEvent;\n import com.intellij.openapi.vfs.VirtualFilePropertyEvent;\n-import com.intellij.openapi.vfs.encoding.EncodingProjectManager;\n+\n+import de.fu_berlin.inf.dpp.SarosPluginContext;\n+import de.fu_berlin.inf.dpp.activities.EditorActivity;\n import de.fu_berlin.inf.dpp.activities.FileActivity;\n-import de.fu_berlin.inf.dpp.activities.FileActivity.Purpose;\n import de.fu_berlin.inf.dpp.activities.FileActivity.Type;\n import de.fu_berlin.inf.dpp.activities.FolderCreatedActivity;\n import de.fu_berlin.inf.dpp.activities.FolderDeletedActivity;\n import de.fu_berlin.inf.dpp.activities.IActivity;\n import de.fu_berlin.inf.dpp.activities.SPath;\n-import de.fu_berlin.inf.dpp.core.util.FileUtils;\n-import de.fu_berlin.inf.dpp.filesystem.IFile;\n-import de.fu_berlin.inf.dpp.filesystem.IFolder;\n import de.fu_berlin.inf.dpp.filesystem.IPath;\n-import de.fu_berlin.inf.dpp.filesystem.IProject;\n-import de.fu_berlin.inf.dpp.filesystem.IResource;\n import de.fu_berlin.inf.dpp.intellij.editor.AbstractStoppableListener;\n import de.fu_berlin.inf.dpp.intellij.editor.EditorManager;\n-import de.fu_berlin.inf.dpp.intellij.editor.StoppableDocumentListener;\n-import de.fu_berlin.inf.dpp.intellij.project.filesystem.IntelliJFileImpl;\n+import de.fu_berlin.inf.dpp.intellij.editor.ProjectAPI;\n+import de.fu_berlin.inf.dpp.intellij.editor.StoppableEditorFileListener;\n+import de.fu_berlin.inf.dpp.intellij.editor.annotations.AnnotationManager;\n+import de.fu_berlin.inf.dpp.intellij.filesystem.VirtualFileConverter;\n import de.fu_berlin.inf.dpp.intellij.project.filesystem.IntelliJPathImpl;\n-import de.fu_berlin.inf.dpp.intellij.project.filesystem.IntelliJProjectImpl;\n+import de.fu_berlin.inf.dpp.intellij.project.filesystem.IntelliJWorkspaceImpl;\n+import de.fu_berlin.inf.dpp.session.ISarosSession;\n import de.fu_berlin.inf.dpp.session.User;\n+\n import org.apache.log4j.Logger;\n import org.jetbrains.annotations.NotNull;\n+import org.jetbrains.annotations.Nullable;\n+import org.picocontainer.annotations.Inject;\n \n-import java.io.File;\n import java.io.IOException;\n-import java.util.ArrayList;\n-import java.util.HashSet;\n-import java.util.List;\n-import java.util.Set;\n+import java.nio.file.Path;\n+import java.nio.file.Paths;\n+import java.util.Deque;\n+import java.util.concurrent.ConcurrentLinkedDeque;\n \n /**\n- * Virtual file system listener. It receives events for all files in all projects\n- * opened by the user.\n- * <p/>\n- * It filters for files that are shared and calls the corresponding methods for\n- * {@link IActivity}-creation on the {@link SharedResourcesManager}.\n+ * Virtual file system listener. It receives events for all files in all\n+ * projects opened by the user.\n+ * It filters for files that are shared and creates the corresponding activities\n+ * dispatches these activities using\n+ * {@link SharedResourcesManager#fireActivity(IActivity)}.\n+ * <p>\n+ * The listener is enabled by default when the session context is created.\n+ * </p>\n+ *\n+ * @see VirtualFileListener\n  */\n+//TODO decouple from SharedResourceManager and add to session context instead\n public class FileSystemChangeListener extends AbstractStoppableListener\n     implements VirtualFileListener {\n \n     private static final Logger LOG = Logger\n         .getLogger(FileSystemChangeListener.class);\n+\n     private final SharedResourcesManager resourceManager;\n+    private final IntelliJWorkspaceImpl intellijWorkspace;\n+    private final ISarosSession session;\n+\n+    @Inject\n+    private ProjectAPI projectAPI;\n \n-    //HACK: This list is used to filter events for files that were created from\n-    //remote, because we can not disable the listener for them\n-    private final List<File> incomingFilesToFilterFor = new ArrayList<File>();\n+    @Inject\n+    private AnnotationManager annotationManager;\n \n-    private final Set<VirtualFile> newFiles = new HashSet<VirtualFile>();\n+    @Inject\n+    private Project project;\n \n     public FileSystemChangeListener(SharedResourcesManager resourceManager,\n-        EditorManager editorManager) {\n+        EditorManager editorManager, IntelliJWorkspaceImpl intellijWorkspace,\n+        ISarosSession session) {\n+\n         super(editorManager);\n-        this.resourceManager = resourceManager;\n-    }\n \n-    private void generateFolderMove(SPath oldSPath, SPath newSPath,\n-        boolean before) {\n-        User user = resourceManager.getSession().getLocalUser();\n-        IntelliJProjectImpl project = (IntelliJProjectImpl) oldSPath\n-            .getProject();\n-        IActivity createActivity = new FolderCreatedActivity(user, newSPath);\n-        resourceManager.internalFireActivity(createActivity);\n+        this.resourceManager = resourceManager;\n+        this.intellijWorkspace = intellijWorkspace;\n+        this.session = session;\n \n-        IFolder folder = before ? oldSPath.getFolder() : newSPath.getFolder();\n+        SarosPluginContext.initComponent(this);\n \n-        IResource[] members = new IResource[0];\n-        try {\n-            members = folder.members();\n-        } catch (IOException e) {\n-            LOG.error(\"error reading folder: \" + folder, e);\n-        }\n+        intellijWorkspace.addResourceListener(this);\n+    }\n \n-        for (IResource resource : members) {\n-            SPath oldChildSPath = new IntelliJFileImpl(project, new File(\n-                oldSPath.getFullPath().toOSString() + File.separator + resource\n-                    .getName())).getSPath();\n-            SPath newChildSPath = new IntelliJFileImpl(\n-                (IntelliJProjectImpl) newSPath.getProject(), new File(\n-                newSPath.getFullPath().toOSString() + File.separator + resource\n-                    .getName())).getSPath();\n-            if (resource.getType() == IResource.FOLDER) {\n-                generateFolderMove(oldChildSPath, newChildSPath, before);\n-            } else {\n-                generateFileMove(oldChildSPath, newChildSPath, before);\n-            }\n-        }\n+    /**\n+     * {@inheritDoc}\n+     * Works for all files in the application scope, including meta-files like\n+     * Intellij configuration files.\n+     * <p></p>\n+     * File changes done though an Intellij editor are processed in the\n+     * {@link StoppableEditorFileListener} instead.\n+     *\n+     * @param virtualFileEvent {@inheritDoc}\n+     * @see StoppableEditorFileListener\n+     */\n+    @Override\n+    public void beforeContentsChange(\n+        @NotNull\n+            VirtualFileEvent virtualFileEvent) {\n \n-        IActivity removeActivity = new FolderDeletedActivity(user, oldSPath);\n-        resourceManager.internalFireActivity(removeActivity);\n+        assert enabled : \"the before contents change listener was triggered while it was disabled\";\n \n-        project.addFile(newSPath.getFile().getLocation().toFile());\n-        project.removeResource(oldSPath.getProjectRelativePath());\n-    }\n+        VirtualFile file = virtualFileEvent.getFile();\n \n-    private void generateFileMove(SPath oldSPath, SPath newSPath,\n-        boolean before) {\n-        User user = resourceManager.getSession().getLocalUser();\n-        IntelliJProjectImpl project = (IntelliJProjectImpl) newSPath\n-            .getProject();\n-        IntelliJProjectImpl oldProject = (IntelliJProjectImpl) oldSPath\n-            .getProject();\n+        if (LOG.isTraceEnabled()) {\n+            LOG.trace(\"Reacting before resource contents changed: \" + file);\n+        }\n \n-        IFile file;\n+        SPath path = VirtualFileConverter.convertToSPath(file);\n \n-        project.addFile(newSPath.getFile().getLocation().toFile());\n+        if (path == null || !session.isShared(path.getResource())) {\n+            if (LOG.isTraceEnabled()) {\n+                LOG.trace(\n+                    \"Ignoring non-shared resource's contents change:\" + file);\n+            }\n \n-        //TODO what happens if the other participant is working on the now renamed file\n-        if (before) {\n-            file = oldProject.getFile(oldSPath.getFullPath());\n-            editorManager.saveDocument(oldSPath);\n+            return;\n+        }\n \n-            editorManager.replaceAllEditorsForPath(oldSPath, newSPath);\n-        } else {\n-            editorManager.replaceAllEditorsForPath(oldSPath, newSPath);\n+        if (virtualFileEvent.isFromSave()) {\n+            if (LOG.isTraceEnabled()) {\n+                LOG.trace(\"Ignoring contents change for \" + file\n+                    + \" as they were caused by a document save.\");\n+            }\n \n-            file = project.getFile(newSPath.getFullPath());\n-            editorManager.saveDocument(newSPath);\n+            //TODO dispatch save activity for saved file\n+            return;\n         }\n \n-        oldProject.removeResource(oldSPath.getProjectRelativePath());\n+        if (virtualFileEvent.isFromRefresh()) {\n+            if (LOG.isTraceEnabled()) {\n+                LOG.trace(\"Ignoring contents change for \" + file\n+                    + \" as they were caused by a filesystem snapshot refresh. \"\n+                    + \"This is already handled by the document listener.\");\n+            }\n \n-        byte[] bytes = FileUtils.getLocalFileContent(file);\n-        String charset = getEncoding(file);\n-        IActivity activity = new FileActivity(user, Type.MOVED,\n-            Purpose.ACTIVITY, newSPath, oldSPath, bytes, charset);\n+            return;\n+        }\n \n-        resourceManager.internalFireActivity(activity);\n+        //TODO figure out if this can happen\n+        LOG.warn(\"Detected unhandled content change on the virtual file level \"\n+            + \"for \" + file + \", requested by: \" + virtualFileEvent\n+            .getRequestor());\n     }\n \n     /**\n-     * Calls {@link EditorManager#sendTemplateContent(SPath, String)} for files\n-     * that were created with initial content. For other content changes itm\n-     * {@link StoppableDocumentListener} is used.\n-     * <p/>\n-     * This gets called for all files in the application, after they were changed.\n-     * This includes meta-files like workspace.xml.\n+     * {@inheritDoc}\n+     * <p></p>\n+     * Generates and dispatches a creation activity for the new resource.\n      *\n-     * @param virtualFileEvent\n+     * @param virtualFileEvent {@inheritDoc}\n      */\n     @Override\n-    public void contentsChanged(\n+    public void fileCreated(\n         @NotNull\n-        VirtualFileEvent virtualFileEvent) {\n-        VirtualFile virtualFile = virtualFileEvent.getFile();\n-        IntelliJProjectImpl project = getProjectForResource(\n-            IntelliJPathImpl.fromString(virtualFile.getPath()));\n+            VirtualFileEvent virtualFileEvent) {\n \n-        if (project == null || !isValidProject(project)) {\n-            return;\n+        assert enabled : \"the file created listener was triggered while it was disabled\";\n+\n+        VirtualFile createdVirtualFile = virtualFileEvent.getFile();\n+\n+        if (LOG.isTraceEnabled()) {\n+            LOG.trace(\"Reacting to resource creation: \" + createdVirtualFile);\n         }\n \n-        IFile file = new IntelliJFileImpl(project,\n-            new File(virtualFile.getPath()));\n+        SPath path = VirtualFileConverter.convertToSPath(createdVirtualFile);\n+\n+        if (path == null || !session.isShared(path.getResource())) {\n+            if (LOG.isTraceEnabled()) {\n+                LOG.trace(\"Ignoring non-shared resource creation: \"\n+                    + createdVirtualFile);\n+            }\n \n-        if (!resourceManager.getSession().isShared(file) && !newFiles\n-            .remove(virtualFile)) {\n             return;\n         }\n \n-        SPath spath = new SPath(project, file.getProjectRelativePath());\n+        User user = session.getLocalUser();\n \n-        //FIXME does not work as it takes the file content, which has the wrong line separators\n-        //Files created from templates have initial content and are opened in\n-        // an editor, but do not have a DocumentListener. Their initial content\n-        // is transferred here, because the DocumentListener is added after\n-        // it was inserted\n-        if (editorManager.isOpenedInEditor(spath)) {\n-            try {\n-                byte[] content = virtualFile.contentsToByteArray();\n-                String initialContent = new String(content, getEncoding(file));\n+        IActivity activity;\n \n-                if (!initialContent.isEmpty()) {\n-                    editorManager.sendTemplateContent(spath, initialContent);\n-                }\n-            } catch (IOException e) {\n-                LOG.error(\"Could not access newly created file: \" + file, e);\n-            }\n+        if (createdVirtualFile.isDirectory()) {\n+            activity = new FolderCreatedActivity(user, path);\n+\n+        } else {\n+            String charset = createdVirtualFile.getCharset().name();\n+\n+            byte[] content = getContent(createdVirtualFile);\n+\n+            activity = new FileActivity(user, Type.CREATED,\n+                FileActivity.Purpose.ACTIVITY, path, null, content, charset);\n+\n+            editorManager.openEditor(path, false);\n         }\n+\n+        fireActivity(activity);\n     }\n \n     /**\n-     * This is called after a file was created on disk, but before optional content\n-     * (e.g. templates) are inserted.\n+     * {@inheritDoc}\n+     * <p></p>\n+     * Generates and dispatches creation activities for copied files. Copied\n+     * directories are handled by {@link #fileCreated(VirtualFileEvent)} and\n+     * contained files are subsequently handled by this listener.\n      *\n-     * @param virtualFileEvent\n+     * @param virtualFileCopyEvent {@inheritDoc}\n      */\n     @Override\n-    public void fileCreated(\n+    public void fileCopied(\n         @NotNull\n-        VirtualFileEvent virtualFileEvent) {\n-        if (!enabled) {\n-            return;\n-        }\n+            VirtualFileCopyEvent virtualFileCopyEvent) {\n \n-        File file = convertVirtualFileEventToFile(virtualFileEvent);\n-        IPath path = IntelliJPathImpl.fromString(file.getPath());\n-        IntelliJProjectImpl project = getProjectForResource(path);\n+        assert enabled : \"the file copied listener was triggered while it was disabled\";\n \n-        if (project == null || !isValidProject(project)) {\n-            return;\n-        }\n+        VirtualFile copy = virtualFileCopyEvent.getFile();\n \n-        //This is true, when a new folder for an incoming project was created.\n-        //If this is the case, we do not want to send an FolderActivity back.\n+        if (copy.isDirectory()) {\n+            LOG.error(\"Unexpected copying event for directory. This should have\"\n+                + \"been handled by the creation listener.\");\n \n-        if (path.equals(project.getLocation())) {\n             return;\n         }\n \n-        if (incomingFilesToFilterFor.remove(file)) {\n-            project.addFile(file);\n-            return;\n+        if (LOG.isTraceEnabled()) {\n+            LOG.trace(\"Reacting to resource copying - original: \"\n+                + virtualFileCopyEvent.getOriginalFile() + \", copy: \" + copy);\n         }\n \n-        if (path.equals(project.getFullPath())) {\n-            if (!isCompletelyShared(project)) {\n-                return;\n-            }\n-        }\n+        SPath copyPath = VirtualFileConverter.convertToSPath(copy);\n \n-        path = makeAbsolutePathProjectRelative(path, project);\n+        if (copyPath == null || !session.isShared(copyPath.getResource())) {\n+            if (LOG.isTraceEnabled()) {\n+                LOG.trace(\"Ignoring non-shared resource copy:\" + copy);\n+            }\n \n-        SPath spath = new SPath(project, path);\n-        User user = resourceManager.getSession().getLocalUser();\n-        IActivity activity;\n+            return;\n+        }\n \n-        if (file.isFile()) {\n-            byte[] bytes = new byte[0];\n-            String charset;\n-            charset = virtualFileEvent.getFile().getCharset().name();\n-            activity = new FileActivity(user, Type.CREATED, Purpose.ACTIVITY,\n-                spath, null, bytes, charset);\n+        User user = session.getLocalUser();\n+        String charset = copy.getCharset().name();\n \n-            //If the file was created with a template, it is filled only later\n-            //so we check for newly created files' content in {@link #contentsChanged},\n-            newFiles.add(virtualFileEvent.getFile());\n-        } else {\n-            activity = new FolderCreatedActivity(user, spath);\n-        }\n+        byte[] content = getContent(copy);\n \n-        project.addFile(file);\n+        IActivity activity = new FileActivity(user, Type.CREATED,\n+            FileActivity.Purpose.ACTIVITY, copyPath, null, content, charset);\n \n-        resourceManager.internalFireActivity(activity);\n+        fireActivity(activity);\n     }\n \n+    /**\n+     * {@inheritDoc}\n+     * <p></p>\n+     * Generates and dispatches a deletion activity for the deleted resource. If\n+     * the resource was a file, subsequently removes any editors for the file\n+     * from the editor pool and drops any held annotations for the file.\n+     *\n+     * @param virtualFileEvent {@inheritDoc}\n+     */\n     @Override\n-    public void fileDeleted(\n+    public void beforeFileDeletion(\n         @NotNull\n-        VirtualFileEvent virtualFileEvent) {\n-        if (!enabled) {\n-            return;\n-        }\n+            VirtualFileEvent virtualFileEvent) {\n \n-        File file = convertVirtualFileEventToFile(virtualFileEvent);\n-        if (incomingFilesToFilterFor.remove(file)) {\n-            return;\n+        assert enabled : \"the before file deletion listener was triggered while it was disabled\";\n+\n+        VirtualFile deletedVirtualFile = virtualFileEvent.getFile();\n+\n+        if (LOG.isTraceEnabled()) {\n+            LOG.trace(\n+                \"Reacting before resource deletion: \" + deletedVirtualFile);\n         }\n \n-        IPath path = IntelliJPathImpl.fromString(file.getPath());\n-        IntelliJProjectImpl project = getProjectForResource(path);\n+        SPath path = VirtualFileConverter.convertToSPath(deletedVirtualFile);\n+\n+        if (path == null || !session.isShared(path.getResource())) {\n+            if (LOG.isTraceEnabled()) {\n+                LOG.trace(\"Ignoring non-shared resource deletion: \"\n+                    + deletedVirtualFile);\n+            }\n \n-        if (project == null || !isValidProject(project) ||\n-            !isCompletelyShared(project)) {\n             return;\n         }\n \n-        path = makeAbsolutePathProjectRelative(path, project);\n-\n-        SPath spath = new SPath(project, path);\n-        User user = resourceManager.getSession().getLocalUser();\n+        User user = session.getLocalUser();\n \n         IActivity activity;\n-        if (virtualFileEvent.getFile().isDirectory()) {\n-            activity = new FolderDeletedActivity(user, spath);\n+\n+        if (deletedVirtualFile.isDirectory()) {\n+            //TODO create deletion activities for child resources\n+            //TODO clean up editor pool and annotations for child resources\n+            activity = new FolderDeletedActivity(user, path);\n+\n         } else {\n-            activity = new FileActivity(user, Type.REMOVED, Purpose.ACTIVITY,\n-                spath, null, null, null);\n+            activity = new FileActivity(user, Type.REMOVED,\n+                FileActivity.Purpose.ACTIVITY, path, null, null, null);\n+\n+            editorManager.removeAllEditorsForPath(path);\n+\n+            annotationManager.removeAnnotations(path.getFile());\n         }\n \n-        project.removeResource(path);\n-        editorManager.removeAllEditorsForPath(spath);\n+        fireActivity(activity);\n \n-        resourceManager.internalFireActivity(activity);\n+        //TODO reset the vector time for the deleted file or contained files if folder\n     }\n \n+    /**\n+     * {@inheritDoc}\n+     * <p></p>\n+     * Generates and dispatches activities handling resources moves.\n+     * <p></p>\n+     * Intellij offers multiple ways of moving resources through the UI that\n+     * are handled in different ways internally:\n+     * <li> <i>Move file to other package:</i></li>\n+     * <p>\n+     * Triggers the move listener for the file.\n+     *\n+     * <li><i>Move package to other package</i> and <i>Move package to other\n+     * source root:</i></li>\n+     * <p>\n+     * Just triggers the move listener for the package directory, does not\n+     * trigger a listener event for the contained files or folders.\n+     * This means the listener has to iterate the contained resources and\n+     * create matching actions in the right order.\n+     *\n+     * <li><i>Move package to other directory:</i></li>\n+     * <p>\n+     * Triggers the create listener for the new path of the contained\n+     * directories in right order. Then triggers the move listener for the\n+     * contained files. Then triggers the delete listener for the old path of\n+     * the contained directories.\n+     *\n+     * @param virtualFileMoveEvent {@inheritDoc}\n+     * @see #generateFileMove(VirtualFile, VirtualFile, VirtualFile, String, String)\n+     * @see #generateFolderMove(VirtualFile, VirtualFile, VirtualFile, String)\n+     * @see #fileCreated(VirtualFileEvent)\n+     * @see #fileDeleted(VirtualFileEvent)\n+     */\n     @Override\n-    public void fileMoved(\n+    public void beforeFileMovement(\n         @NotNull\n-        VirtualFileMoveEvent virtualFileMoveEvent) {\n-        if (!enabled) {\n-            return;\n-        }\n+            VirtualFileMoveEvent virtualFileMoveEvent) {\n \n-        File newFile = convertVirtualFileEventToFile(virtualFileMoveEvent);\n-        if (incomingFilesToFilterFor.remove(newFile)) {\n-            return;\n+        assert enabled : \"the before file move listener was triggered while it was disabled\";\n+\n+        VirtualFile oldFile = virtualFileMoveEvent.getFile();\n+        VirtualFile oldParent = virtualFileMoveEvent.getOldParent();\n+        VirtualFile newParent = virtualFileMoveEvent.getNewParent();\n+\n+        if (LOG.isTraceEnabled()) {\n+            LOG.trace(\"Reacting before resource move - resource: \" + oldFile\n+                + \", old parent: \" + oldParent + \", new Parent: \" + newParent);\n         }\n \n-        IPath path = IntelliJPathImpl.fromString(newFile.getPath());\n-        IntelliJProjectImpl project = getProjectForResource(path);\n+        if (oldFile.isDirectory()) {\n+            generateFolderMove(oldFile, oldParent, newParent, null);\n \n-        if (project == null || !isValidProject(project) ||\n-            !isCompletelyShared(project)) {\n-            return;\n+        } else {\n+            generateFileMove(oldFile, oldParent, newParent, null, null);\n         }\n+    }\n+\n+    /**\n+     * Generates and dispatches matching creation and deletion activities\n+     * replicating moving the given directory. Also dispatches activities for\n+     * all contained resources belonging to the same module.\n+     * <p></p>\n+     * How the resources (including the given directory) are handled depends on\n+     * whether the source and target directory is shared.\n+     * <ul>\n+     * <li>\n+     * If only the source directory is shared, only deletion activities for the\n+     * old resources are dispatched.\n+     * </li>\n+     * <li>\n+     * If only the target directory is shared, only creation activities for the\n+     * new resources are dispatched.\n+     * </li>\n+     * <li>\n+     * If both directories are shared, the resources are moved as follows:\n+     * <p>\n+     * Creation activities for the new directories (located in the target\n+     * directory) and move activities for the contained files are dispatched in\n+     * increasing depth order. Then, deletion activities for the old directories\n+     * (in the source directory) are dispatched in decreasing depth order. This\n+     * ensures that any resource move is already handled correctly before one of\n+     * its parent resources is deleted.\n+     * </li>\n+     * </ul>\n+     *\n+     * <p></p>\n+     * Renaming directories is also handled as a move activity. This can be done\n+     * with the optional parameter <code>newFolderName</code>.\n+     *\n+     * @param oldFile       the directory that is about to be moved\n+     * @param oldParent     the old parent of the moved file\n+     * @param newParent     the new parent of the moved file\n+     * @param newFolderName the new name for the folder or null if the folder\n+     *                      was not renamed\n+     * @see #generateFileMove(VirtualFile, VirtualFile, VirtualFile, String, String)\n+     */\n+    private void generateFolderMove(\n+        @NotNull\n+            VirtualFile oldFile,\n+        @NotNull\n+            VirtualFile oldParent,\n+        @NotNull\n+            VirtualFile newParent,\n+        @Nullable\n+            String newFolderName) {\n \n-        path = makeAbsolutePathProjectRelative(path, project);\n+        String folderName =\n+            newFolderName != null ? newFolderName : oldFile.getName();\n \n-        SPath newSPath = new SPath(project, path);\n+        SPath oldPath = VirtualFileConverter.convertToSPath(oldFile);\n+        SPath newParentPath = VirtualFileConverter.convertToSPath(newParent);\n \n-        IPath oldParent = IntelliJPathImpl\n-            .fromString(virtualFileMoveEvent.getOldParent().getPath());\n-        IPath oldPath = oldParent.append(virtualFileMoveEvent.getFileName());\n-        IProject oldProject = getProjectForResource(oldPath);\n+        User user = session.getLocalUser();\n \n-        oldPath = makeAbsolutePathProjectRelative(oldPath, project);\n-        SPath oldSPath = new SPath(oldProject, oldPath);\n+        boolean oldPathIsShared =\n+            oldPath != null && session.isShared(oldPath.getResource());\n+        boolean newPathIsShared = newParentPath != null && session\n+            .isShared(newParentPath.getResource());\n \n-        //FIXME: Handle cases where files are moved from outside the shared project\n-        //into the shared project\n-        if (oldProject == null) {\n-            LOG.error(\n-                \" can not move files from unshared project to shared project\");\n-            return;\n-        }\n-        if (project.equals(oldProject)) {\n-            if (newFile.isFile()) {\n-                generateFileMove(oldSPath, newSPath, false);\n-            } else {\n-                generateFolderMove(oldSPath, newSPath, false);\n+        if (!oldPathIsShared && !newPathIsShared) {\n+            if (LOG.isTraceEnabled()) {\n+                LOG.trace(\n+                    \"Ignoring non-shared resource move - resource: \" + oldFile\n+                        + \", old parent: \" + oldParent + \", new Parent: \"\n+                        + newParent);\n             }\n-        }\n-    }\n \n-    @Override\n-    public void propertyChanged(\n-        @NotNull\n-        VirtualFilePropertyEvent filePropertyEvent) {\n-        if (!enabled) {\n             return;\n         }\n \n-        File oldFile = new File(\n-            filePropertyEvent.getFile().getParent().getPath() + File.separator\n-                + filePropertyEvent.getOldValue());\n-        File newFile = convertVirtualFileEventToFile(filePropertyEvent);\n+        Deque<IActivity> queuedDeletionActivities = new ConcurrentLinkedDeque<>();\n+\n+        /*\n+         * Filter determining which child resources to iterate over.\n+         * It only iterated over resources belonging to the same module as the\n+         * base directory. This is needed to exclude submodules.\n+         *\n+         * TODO once sharing multiple modules is possible, introduce logic to handle deleted submodules if they are shared\n+         */\n+        VirtualFileFilter virtualFileFilter = file -> {\n+            Module baseModule = ModuleUtil.findModuleForFile(oldFile, project);\n+            Module module = ModuleUtil.findModuleForFile(file, project);\n+\n+            return baseModule != null && baseModule.equals(module);\n+        };\n+\n+        /*\n+         * Defines the actions executed on the base directory and every valid\n+         * child resource (defined through the virtualFileFilter).\n+         *\n+         * Returns whether the content iteration should be continued.\n+         */\n+        ContentIterator contentIterator = fileOrDir -> {\n+\n+            IPath relativePath = getRelativePath(oldFile, fileOrDir);\n+\n+            if (relativePath == null) {\n+                return true;\n+            }\n \n-        if (incomingFilesToFilterFor.remove(newFile)) {\n-            return;\n-        }\n+            if (!fileOrDir.isDirectory()) {\n+                generateFileMove(fileOrDir, oldParent, newParent, newFolderName,\n+                    null);\n \n-        IPath oldPath = IntelliJPathImpl.fromString(oldFile.getPath());\n-        IntelliJProjectImpl project = getProjectForResource(oldPath);\n+                return true;\n+            }\n \n-        if (project == null || !isValidProject(project)) {\n-            return;\n-        }\n+            if (newPathIsShared) {\n+                SPath newFolderPath = new SPath(newParentPath.getProject(),\n+                    newParentPath.getProjectRelativePath().append(folderName)\n+                        .append(relativePath));\n \n-        oldPath = makeAbsolutePathProjectRelative(oldPath, project);\n-        SPath oldSPath = new SPath(project, oldPath);\n+                IActivity newFolderCreatedActivity = new FolderCreatedActivity(\n+                    user, newFolderPath);\n \n-        IPath newPath = IntelliJPathImpl.fromString(newFile.getPath());\n-        newPath = makeAbsolutePathProjectRelative(newPath, project);\n+                fireActivity(newFolderCreatedActivity);\n+            }\n \n-        SPath newSPath = new SPath(project, newPath);\n-        //we handle this as a move activity\n-        if (newFile.isFile()) {\n-            generateFileMove(oldSPath, newSPath, false);\n-        } else {\n-            generateFolderMove(oldSPath, newSPath, false);\n+            if (oldPathIsShared) {\n+                SPath oldFolderPath = new SPath(oldPath.getProject(),\n+                    oldPath.getProjectRelativePath().append(relativePath));\n+\n+                IActivity newFolderDeletedActivity = new FolderDeletedActivity(\n+                    user, oldFolderPath);\n+\n+                queuedDeletionActivities.addFirst(newFolderDeletedActivity);\n+            }\n+\n+            return true;\n+        };\n+\n+        /*\n+         * Calls the above defined contentIterator on the base directory and\n+         * every contained resource.\n+         * Directories are only stepped into if they match the above defined\n+         * virtualFileFilter. This also applies to the base directory.\n+         */\n+        VfsUtilCore.iterateChildrenRecursively(oldFile, virtualFileFilter,\n+            contentIterator);\n+\n+        while (!queuedDeletionActivities.isEmpty()) {\n+            fireActivity(queuedDeletionActivities.pop());\n         }\n     }\n \n-    @Override\n-    public void fileCopied(\n+    /**\n+     * How the moved file is handled depends on whether the source and target\n+     * directory are shared:\n+     * <ul>\n+     * <li>\n+     * If only the source directory is shared, a deletion activity for the\n+     * old file is created and dispatched. Subsequently removes any editor for\n+     * the file from the editor pool and drops any annotation held for the file.\n+     * </li>\n+     * <li>\n+     * If only the target directory is shared, a creation activity for the new\n+     * file is created and dispatched. Subsequently adds an entry for the new\n+     * file to the editor pool if it is currently open in an editor.\n+     * </li>\n+     * <li>\n+     * If both the source and target directory are shared, a move activity for\n+     * the file is created and dispatched. Subsequently updates the path of any\n+     * editor for the file in the editor pool and updates the path for any held\n+     * annotations for the file.\n+     * </li>\n+     * </ul>\n+     * If the source directory is shared, an editor closed activity is also\n+     * dispatched. If the target directory is shared, an editor opened activity\n+     * is also dispatched. This is necessary as Intellij does not close and\n+     * re-open the editors of moved files but rather changes the path of the\n+     * editor internally, meaning the <code>UserEditorState</code> held by the\n+     * other participants would not be updated correctly.\n+     * <p></p>\n+     * Renaming files is also handled as a move activity. This can be done\n+     * with the optional parameter <code>newFileName</code>.\n+     * <p></p>\n+     * The optional parameter <code>newBaseName</code> can be used to handle\n+     * cases where a parent file is renamed. The renamed directory has to be the\n+     * directory located under <code>oldBaseParent</code> in the old path.\n+     *\n+     * @param oldFile       the file that is about to be moved\n+     * @param oldBaseParent the old base parent of the file\n+     * @param newBaseParent the new base parent of the file\n+     * @param newBaseName   the new name for the base parent or null if it was\n+     *                      not renamed\n+     * @param newFileName   the new name for the file or null if it was not\n+     *                      renamed\n+     * @see #generateFolderMove(VirtualFile, VirtualFile, VirtualFile, String)\n+     */\n+    private void generateFileMove(","path":"de.fu_berlin.inf.dpp.intellij/src/de/fu_berlin/inf/dpp/intellij/project/FileSystemChangeListener.java","position":813,"original_position":813,"commit_id":"d0856637f6d0a16d34e934f53e9982eeed48162e","original_commit_id":"d0856637f6d0a16d34e934f53e9982eeed48162e","user":{"login":"saros-infrastructure","id":39915837,"node_id":"MDQ6VXNlcjM5OTE1ODM3","avatar_url":"https://avatars1.githubusercontent.com/u/39915837?v=4","gravatar_id":"","url":"https://api.github.com/users/saros-infrastructure","html_url":"https://github.com/saros-infrastructure","followers_url":"https://api.github.com/users/saros-infrastructure/followers","following_url":"https://api.github.com/users/saros-infrastructure/following{/other_user}","gists_url":"https://api.github.com/users/saros-infrastructure/gists{/gist_id}","starred_url":"https://api.github.com/users/saros-infrastructure/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/saros-infrastructure/subscriptions","organizations_url":"https://api.github.com/users/saros-infrastructure/orgs","repos_url":"https://api.github.com/users/saros-infrastructure/repos","events_url":"https://api.github.com/users/saros-infrastructure/events{/privacy}","received_events_url":"https://api.github.com/users/saros-infrastructure/received_events","type":"User","site_admin":false},"body":"![Code Smell](https://sonarsource.github.io/sonar-github/code_smell.png 'Code Smell') Code Smell: Refactor this method to reduce its Cognitive Complexity from 20 to the 15 allowed. ([squid:S3776](https://sonarcloud.io/organizations/saros/rules#rule_key=squid%3AS3776))\n\n[See it in SonarCloud](https://sonarcloud.io/project/issues?id=saros_complete&pullRequest=307&issues=AWcYB0JQkEeD8QRk19vT&open=AWcYB0JQkEeD8QRk19vT)\n\n[ISSUE_KEY]: AWcYB0JQkEeD8QRk19vT","created_at":"2018-11-15T15:39:51Z","updated_at":"2018-11-15T15:39:51Z","html_url":"https://github.com/saros-project/saros/pull/307#discussion_r233893516","pull_request_url":"https://api.github.com/repos/saros-project/saros/pulls/307","author_association":"NONE","_links":{"self":{"href":"https://api.github.com/repos/saros-project/saros/pulls/comments/233893516"},"html":{"href":"https://github.com/saros-project/saros/pull/307#discussion_r233893516"},"pull_request":{"href":"https://api.github.com/repos/saros-project/saros/pulls/307"}}},"pull_request":{"url":"https://api.github.com/repos/saros-project/saros/pulls/307","id":231235403,"node_id":"MDExOlB1bGxSZXF1ZXN0MjMxMjM1NDAz","html_url":"https://github.com/saros-project/saros/pull/307","diff_url":"https://github.com/saros-project/saros/pull/307.diff","patch_url":"https://github.com/saros-project/saros/pull/307.patch","issue_url":"https://api.github.com/repos/saros-project/saros/issues/307","number":307,"state":"open","locked":false,"title":"[I] Rewrite FileSystemChangeListener","user":{"login":"tobous","id":12495415,"node_id":"MDQ6VXNlcjEyNDk1NDE1","avatar_url":"https://avatars1.githubusercontent.com/u/12495415?v=4","gravatar_id":"","url":"https://api.github.com/users/tobous","html_url":"https://github.com/tobous","followers_url":"https://api.github.com/users/tobous/followers","following_url":"https://api.github.com/users/tobous/following{/other_user}","gists_url":"https://api.github.com/users/tobous/gists{/gist_id}","starred_url":"https://api.github.com/users/tobous/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tobous/subscriptions","organizations_url":"https://api.github.com/users/tobous/orgs","repos_url":"https://api.github.com/users/tobous/repos","events_url":"https://api.github.com/users/tobous/events{/privacy}","received_events_url":"https://api.github.com/users/tobous/received_events","type":"User","site_admin":false},"body":"Completely rewrites the handling of local filesystem activities. Since this is a rather large change, it is split into multiple commits to make the review process a bit easier.","created_at":"2018-11-15T15:30:07Z","updated_at":"2018-11-15T15:39:51Z","closed_at":null,"merged_at":null,"merge_commit_sha":"154d60d59c7c5b0dc9cf676cd1cc8aad7ac5fef9","assignee":{"login":"tobous","id":12495415,"node_id":"MDQ6VXNlcjEyNDk1NDE1","avatar_url":"https://avatars1.githubusercontent.com/u/12495415?v=4","gravatar_id":"","url":"https://api.github.com/users/tobous","html_url":"https://github.com/tobous","followers_url":"https://api.github.com/users/tobous/followers","following_url":"https://api.github.com/users/tobous/following{/other_user}","gists_url":"https://api.github.com/users/tobous/gists{/gist_id}","starred_url":"https://api.github.com/users/tobous/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tobous/subscriptions","organizations_url":"https://api.github.com/users/tobous/orgs","repos_url":"https://api.github.com/users/tobous/repos","events_url":"https://api.github.com/users/tobous/events{/privacy}","received_events_url":"https://api.github.com/users/tobous/received_events","type":"User","site_admin":false},"assignees":[{"login":"tobous","id":12495415,"node_id":"MDQ6VXNlcjEyNDk1NDE1","avatar_url":"https://avatars1.githubusercontent.com/u/12495415?v=4","gravatar_id":"","url":"https://api.github.com/users/tobous","html_url":"https://github.com/tobous","followers_url":"https://api.github.com/users/tobous/followers","following_url":"https://api.github.com/users/tobous/following{/other_user}","gists_url":"https://api.github.com/users/tobous/gists{/gist_id}","starred_url":"https://api.github.com/users/tobous/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tobous/subscriptions","organizations_url":"https://api.github.com/users/tobous/orgs","repos_url":"https://api.github.com/users/tobous/repos","events_url":"https://api.github.com/users/tobous/events{/privacy}","received_events_url":"https://api.github.com/users/tobous/received_events","type":"User","site_admin":false}],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":881373676,"node_id":"MDU6TGFiZWw4ODEzNzM2NzY=","url":"https://api.github.com/repos/saros-project/saros/labels/intellij","name":"intellij","color":"fbca04","default":false}],"milestone":null,"commits_url":"https://api.github.com/repos/saros-project/saros/pulls/307/commits","review_comments_url":"https://api.github.com/repos/saros-project/saros/pulls/307/comments","review_comment_url":"https://api.github.com/repos/saros-project/saros/pulls/comments{/number}","comments_url":"https://api.github.com/repos/saros-project/saros/issues/307/comments","statuses_url":"https://api.github.com/repos/saros-project/saros/statuses/d0856637f6d0a16d34e934f53e9982eeed48162e","head":{"label":"saros-project:pr/intellij/rework_filesystemchangelistener","ref":"pr/intellij/rework_filesystemchangelistener","sha":"d0856637f6d0a16d34e934f53e9982eeed48162e","user":{"login":"saros-project","id":1419284,"node_id":"MDEyOk9yZ2FuaXphdGlvbjE0MTkyODQ=","avatar_url":"https://avatars1.githubusercontent.com/u/1419284?v=4","gravatar_id":"","url":"https://api.github.com/users/saros-project","html_url":"https://github.com/saros-project","followers_url":"https://api.github.com/users/saros-project/followers","following_url":"https://api.github.com/users/saros-project/following{/other_user}","gists_url":"https://api.github.com/users/saros-project/gists{/gist_id}","starred_url":"https://api.github.com/users/saros-project/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/saros-project/subscriptions","organizations_url":"https://api.github.com/users/saros-project/orgs","repos_url":"https://api.github.com/users/saros-project/repos","events_url":"https://api.github.com/users/saros-project/events{/privacy}","received_events_url":"https://api.github.com/users/saros-project/received_events","type":"Organization","site_admin":false},"repo":{"id":11182907,"node_id":"MDEwOlJlcG9zaXRvcnkxMTE4MjkwNw==","name":"saros","full_name":"saros-project/saros","private":false,"owner":{"login":"saros-project","id":1419284,"node_id":"MDEyOk9yZ2FuaXphdGlvbjE0MTkyODQ=","avatar_url":"https://avatars1.githubusercontent.com/u/1419284?v=4","gravatar_id":"","url":"https://api.github.com/users/saros-project","html_url":"https://github.com/saros-project","followers_url":"https://api.github.com/users/saros-project/followers","following_url":"https://api.github.com/users/saros-project/following{/other_user}","gists_url":"https://api.github.com/users/saros-project/gists{/gist_id}","starred_url":"https://api.github.com/users/saros-project/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/saros-project/subscriptions","organizations_url":"https://api.github.com/users/saros-project/orgs","repos_url":"https://api.github.com/users/saros-project/repos","events_url":"https://api.github.com/users/saros-project/events{/privacy}","received_events_url":"https://api.github.com/users/saros-project/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/saros-project/saros","description":"Saros Plugin","fork":false,"url":"https://api.github.com/repos/saros-project/saros","forks_url":"https://api.github.com/repos/saros-project/saros/forks","keys_url":"https://api.github.com/repos/saros-project/saros/keys{/key_id}","collaborators_url":"https://api.github.com/repos/saros-project/saros/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/saros-project/saros/teams","hooks_url":"https://api.github.com/repos/saros-project/saros/hooks","issue_events_url":"https://api.github.com/repos/saros-project/saros/issues/events{/number}","events_url":"https://api.github.com/repos/saros-project/saros/events","assignees_url":"https://api.github.com/repos/saros-project/saros/assignees{/user}","branches_url":"https://api.github.com/repos/saros-project/saros/branches{/branch}","tags_url":"https://api.github.com/repos/saros-project/saros/tags","blobs_url":"https://api.github.com/repos/saros-project/saros/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/saros-project/saros/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/saros-project/saros/git/refs{/sha}","trees_url":"https://api.github.com/repos/saros-project/saros/git/trees{/sha}","statuses_url":"https://api.github.com/repos/saros-project/saros/statuses/{sha}","languages_url":"https://api.github.com/repos/saros-project/saros/languages","stargazers_url":"https://api.github.com/repos/saros-project/saros/stargazers","contributors_url":"https://api.github.com/repos/saros-project/saros/contributors","subscribers_url":"https://api.github.com/repos/saros-project/saros/subscribers","subscription_url":"https://api.github.com/repos/saros-project/saros/subscription","commits_url":"https://api.github.com/repos/saros-project/saros/commits{/sha}","git_commits_url":"https://api.github.com/repos/saros-project/saros/git/commits{/sha}","comments_url":"https://api.github.com/repos/saros-project/saros/comments{/number}","issue_comment_url":"https://api.github.com/repos/saros-project/saros/issues/comments{/number}","contents_url":"https://api.github.com/repos/saros-project/saros/contents/{+path}","compare_url":"https://api.github.com/repos/saros-project/saros/compare/{base}...{head}","merges_url":"https://api.github.com/repos/saros-project/saros/merges","archive_url":"https://api.github.com/repos/saros-project/saros/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/saros-project/saros/downloads","issues_url":"https://api.github.com/repos/saros-project/saros/issues{/number}","pulls_url":"https://api.github.com/repos/saros-project/saros/pulls{/number}","milestones_url":"https://api.github.com/repos/saros-project/saros/milestones{/number}","notifications_url":"https://api.github.com/repos/saros-project/saros/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/saros-project/saros/labels{/name}","releases_url":"https://api.github.com/repos/saros-project/saros/releases{/id}","deployments_url":"https://api.github.com/repos/saros-project/saros/deployments","created_at":"2013-07-04T17:14:07Z","updated_at":"2018-11-14T11:45:51Z","pushed_at":"2018-11-15T15:33:30Z","git_url":"git://github.com/saros-project/saros.git","ssh_url":"git@github.com:saros-project/saros.git","clone_url":"https://github.com/saros-project/saros.git","svn_url":"https://github.com/saros-project/saros","homepage":"http://www.saros-project.org","size":143738,"stargazers_count":59,"watchers_count":59,"language":"Java","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":27,"mirror_url":null,"archived":false,"open_issues_count":177,"license":{"key":"gpl-2.0","name":"GNU General Public License v2.0","spdx_id":"GPL-2.0","url":"https://api.github.com/licenses/gpl-2.0","node_id":"MDc6TGljZW5zZTg="},"forks":27,"open_issues":177,"watchers":59,"default_branch":"master"}},"base":{"label":"saros-project:pr/intellij/static_virtualfileconverter","ref":"pr/intellij/static_virtualfileconverter","sha":"13b6059acbdf18ae12b3ed6ea594be6be24beb98","user":{"login":"saros-project","id":1419284,"node_id":"MDEyOk9yZ2FuaXphdGlvbjE0MTkyODQ=","avatar_url":"https://avatars1.githubusercontent.com/u/1419284?v=4","gravatar_id":"","url":"https://api.github.com/users/saros-project","html_url":"https://github.com/saros-project","followers_url":"https://api.github.com/users/saros-project/followers","following_url":"https://api.github.com/users/saros-project/following{/other_user}","gists_url":"https://api.github.com/users/saros-project/gists{/gist_id}","starred_url":"https://api.github.com/users/saros-project/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/saros-project/subscriptions","organizations_url":"https://api.github.com/users/saros-project/orgs","repos_url":"https://api.github.com/users/saros-project/repos","events_url":"https://api.github.com/users/saros-project/events{/privacy}","received_events_url":"https://api.github.com/users/saros-project/received_events","type":"Organization","site_admin":false},"repo":{"id":11182907,"node_id":"MDEwOlJlcG9zaXRvcnkxMTE4MjkwNw==","name":"saros","full_name":"saros-project/saros","private":false,"owner":{"login":"saros-project","id":1419284,"node_id":"MDEyOk9yZ2FuaXphdGlvbjE0MTkyODQ=","avatar_url":"https://avatars1.githubusercontent.com/u/1419284?v=4","gravatar_id":"","url":"https://api.github.com/users/saros-project","html_url":"https://github.com/saros-project","followers_url":"https://api.github.com/users/saros-project/followers","following_url":"https://api.github.com/users/saros-project/following{/other_user}","gists_url":"https://api.github.com/users/saros-project/gists{/gist_id}","starred_url":"https://api.github.com/users/saros-project/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/saros-project/subscriptions","organizations_url":"https://api.github.com/users/saros-project/orgs","repos_url":"https://api.github.com/users/saros-project/repos","events_url":"https://api.github.com/users/saros-project/events{/privacy}","received_events_url":"https://api.github.com/users/saros-project/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/saros-project/saros","description":"Saros Plugin","fork":false,"url":"https://api.github.com/repos/saros-project/saros","forks_url":"https://api.github.com/repos/saros-project/saros/forks","keys_url":"https://api.github.com/repos/saros-project/saros/keys{/key_id}","collaborators_url":"https://api.github.com/repos/saros-project/saros/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/saros-project/saros/teams","hooks_url":"https://api.github.com/repos/saros-project/saros/hooks","issue_events_url":"https://api.github.com/repos/saros-project/saros/issues/events{/number}","events_url":"https://api.github.com/repos/saros-project/saros/events","assignees_url":"https://api.github.com/repos/saros-project/saros/assignees{/user}","branches_url":"https://api.github.com/repos/saros-project/saros/branches{/branch}","tags_url":"https://api.github.com/repos/saros-project/saros/tags","blobs_url":"https://api.github.com/repos/saros-project/saros/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/saros-project/saros/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/saros-project/saros/git/refs{/sha}","trees_url":"https://api.github.com/repos/saros-project/saros/git/trees{/sha}","statuses_url":"https://api.github.com/repos/saros-project/saros/statuses/{sha}","languages_url":"https://api.github.com/repos/saros-project/saros/languages","stargazers_url":"https://api.github.com/repos/saros-project/saros/stargazers","contributors_url":"https://api.github.com/repos/saros-project/saros/contributors","subscribers_url":"https://api.github.com/repos/saros-project/saros/subscribers","subscription_url":"https://api.github.com/repos/saros-project/saros/subscription","commits_url":"https://api.github.com/repos/saros-project/saros/commits{/sha}","git_commits_url":"https://api.github.com/repos/saros-project/saros/git/commits{/sha}","comments_url":"https://api.github.com/repos/saros-project/saros/comments{/number}","issue_comment_url":"https://api.github.com/repos/saros-project/saros/issues/comments{/number}","contents_url":"https://api.github.com/repos/saros-project/saros/contents/{+path}","compare_url":"https://api.github.com/repos/saros-project/saros/compare/{base}...{head}","merges_url":"https://api.github.com/repos/saros-project/saros/merges","archive_url":"https://api.github.com/repos/saros-project/saros/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/saros-project/saros/downloads","issues_url":"https://api.github.com/repos/saros-project/saros/issues{/number}","pulls_url":"https://api.github.com/repos/saros-project/saros/pulls{/number}","milestones_url":"https://api.github.com/repos/saros-project/saros/milestones{/number}","notifications_url":"https://api.github.com/repos/saros-project/saros/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/saros-project/saros/labels{/name}","releases_url":"https://api.github.com/repos/saros-project/saros/releases{/id}","deployments_url":"https://api.github.com/repos/saros-project/saros/deployments","created_at":"2013-07-04T17:14:07Z","updated_at":"2018-11-14T11:45:51Z","pushed_at":"2018-11-15T15:33:30Z","git_url":"git://github.com/saros-project/saros.git","ssh_url":"git@github.com:saros-project/saros.git","clone_url":"https://github.com/saros-project/saros.git","svn_url":"https://github.com/saros-project/saros","homepage":"http://www.saros-project.org","size":143738,"stargazers_count":59,"watchers_count":59,"language":"Java","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":27,"mirror_url":null,"archived":false,"open_issues_count":177,"license":{"key":"gpl-2.0","name":"GNU General Public License v2.0","spdx_id":"GPL-2.0","url":"https://api.github.com/licenses/gpl-2.0","node_id":"MDc6TGljZW5zZTg="},"forks":27,"open_issues":177,"watchers":59,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/saros-project/saros/pulls/307"},"html":{"href":"https://github.com/saros-project/saros/pull/307"},"issue":{"href":"https://api.github.com/repos/saros-project/saros/issues/307"},"comments":{"href":"https://api.github.com/repos/saros-project/saros/issues/307/comments"},"review_comments":{"href":"https://api.github.com/repos/saros-project/saros/pulls/307/comments"},"review_comment":{"href":"https://api.github.com/repos/saros-project/saros/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/saros-project/saros/pulls/307/commits"},"statuses":{"href":"https://api.github.com/repos/saros-project/saros/statuses/d0856637f6d0a16d34e934f53e9982eeed48162e"}},"author_association":"MEMBER"}}
{"actor":{"display_login":"saros-infrastructure"}}