Skip to content

Commit

Permalink
CHE-10696: Rework GitStatusProvider to use cached status storage
Browse files Browse the repository at this point in the history
  • Loading branch information
vinokurig committed Aug 22, 2018
1 parent 2b94f41 commit 30720b4
Show file tree
Hide file tree
Showing 7 changed files with 407 additions and 135 deletions.
Expand Up @@ -126,12 +126,16 @@ public void testUntrackedFileColor() {

@Test(priority = 1)
public void testUntrackedFileColorFromTerminal() {
projectExplorer.openItemByPath(PROJECT_NAME);
editor.closeAllTabs();

// Remove file from index
terminal.selectFirstTerminalTab();
terminal.typeIntoActiveTerminal("cd " + PROJECT_NAME + Keys.ENTER);
terminal.typeIntoActiveTerminal("git rm --cached README.md" + Keys.ENTER);

// Check file colors are yellow
projectExplorer.openItemByPath(PROJECT_NAME);
projectExplorer.waitYellowNode(PROJECT_NAME + "/README.md");
editor.waitYellowTab("README.md");

Expand Down
@@ -0,0 +1,290 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.api.git;

import static java.nio.file.Files.getLastModifiedTime;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toSet;
import static org.eclipse.che.api.fs.server.WsPathUtils.SEPARATOR;
import static org.eclipse.che.api.fs.server.WsPathUtils.absolutize;
import static org.eclipse.che.api.fs.server.WsPathUtils.resolve;
import static org.eclipse.che.api.project.server.VcsStatusProvider.VcsStatus.ADDED;
import static org.eclipse.che.api.project.server.VcsStatusProvider.VcsStatus.MODIFIED;
import static org.eclipse.che.api.project.server.VcsStatusProvider.VcsStatus.NOT_MODIFIED;
import static org.eclipse.che.api.project.server.VcsStatusProvider.VcsStatus.UNTRACKED;
import static org.eclipse.che.dto.server.DtoFactory.newDto;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.model.workspace.config.ProjectConfig;
import org.eclipse.che.api.core.notification.EventService;
import org.eclipse.che.api.fs.server.PathTransformer;
import org.eclipse.che.api.git.exception.GitException;
import org.eclipse.che.api.git.shared.FileChangedEventDto;
import org.eclipse.che.api.git.shared.Status;
import org.eclipse.che.api.git.shared.StatusChangedEventDto;
import org.eclipse.che.api.project.server.ProjectManager;
import org.eclipse.che.api.project.server.VcsStatusProvider;
import org.eclipse.che.api.project.server.impl.RootDirPathProvider;
import org.eclipse.che.api.project.server.notification.ProjectCreatedEvent;
import org.eclipse.che.api.project.server.notification.ProjectDeletedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Git implementation of {@link VcsStatusProvider} based on a {@link Map} which contains statuses of
* all workspace projects. The map is updated by Git events and by checking the file's modification
* time to update the map if the files were changed bypassing the file-watchers e.g. the file wasn't
* opened neither in the project explorer tree, neither in the editor, but was changed in the
* terminal.
*
* @author Igor Vinokur
*/
public class CachedGitStatusProvider implements VcsStatusProvider {

private static final Logger LOG = LoggerFactory.getLogger(CachedGitStatusProvider.class);

private final GitConnectionFactory gitConnectionFactory;
private final PathTransformer pathTransformer;
private final ProjectManager projectManager;
private final RootDirPathProvider rootDirPathProvider;
private final EventService eventService;
private final Map<String, Status> statusCache;
private final Map<String, FileTime> projectFiles;

@Inject
public CachedGitStatusProvider(
GitConnectionFactory gitConnectionFactory,
PathTransformer pathTransformer,
ProjectManager projectManager,
RootDirPathProvider rootDirPathProvider,
EventService eventService) {
this.gitConnectionFactory = gitConnectionFactory;
this.pathTransformer = pathTransformer;
this.projectManager = projectManager;
this.rootDirPathProvider = rootDirPathProvider;
this.eventService = eventService;
this.statusCache = new HashMap<>();
this.projectFiles = new HashMap<>();

subscribeToEvents();
collectProjectFiles(rootDirPathProvider.get());
}

private void collectProjectFiles(String root) {
try {
Set<Path> filePaths =
Files.walk(Paths.get(root)).filter(Files::isRegularFile).collect(toSet());
for (Path path : filePaths) {
String filePath = path.toString();
projectFiles.put(filePath, getLastModifiedTime(Paths.get(filePath)));
}
} catch (IOException exception) {
LOG.error(exception.getMessage());
}
}

private void subscribeToEvents() {
eventService.subscribe(
event -> statusCache.put(event.getProjectName(), event.getStatus()),
StatusChangedEventDto.class);

eventService.subscribe(
event -> {
String filePath = event.getPath();
FileChangedEventDto.Status status = event.getStatus();
Status statusDto = newDto(Status.class);
if (status == FileChangedEventDto.Status.ADDED) {
statusDto.setAdded(singletonList(filePath));
} else if (status == FileChangedEventDto.Status.MODIFIED) {
statusDto.setModified(singletonList(filePath));
} else if (status == FileChangedEventDto.Status.UNTRACKED) {
statusDto.setModified(singletonList(filePath));
}

updateCachedStatus(
Paths.get(filePath).getParent().getFileName().toString(),
singletonList(filePath),
statusDto);

try {
projectFiles.put(
filePath, getLastModifiedTime(Paths.get(rootDirPathProvider.get() + filePath)));
} catch (IOException exception) {
LOG.error(exception.getMessage());
}
},
FileChangedEventDto.class);

eventService.subscribe(
event -> collectProjectFiles(pathTransformer.transform(event.getProjectPath()).toString()),
ProjectCreatedEvent.class);

eventService.subscribe(
event -> {
String projectFsPath = pathTransformer.transform(event.getProjectPath()).toString();
projectFiles.keySet().removeIf(file -> file.startsWith(projectFsPath));
statusCache.remove(
event.getProjectPath().substring(event.getProjectPath().lastIndexOf('/') + 1));
},
ProjectDeletedEvent.class);
}

@Override
public String getVcsName() {
return GitProjectType.TYPE_ID;
}

@Override
public VcsStatus getStatus(String wsPath) throws ServerException {
try {
ProjectConfig project =
projectManager
.getClosest(wsPath)
.orElseThrow(() -> new NotFoundException("Can't find project"));
wsPath = wsPath.substring(wsPath.startsWith(SEPARATOR) ? 1 : 0);
String itemPath = wsPath.substring(wsPath.indexOf(SEPARATOR) + 1);

Status status =
getStatus(
project.getName(),
pathTransformer.transform(project.getPath()).toString(),
singletonList(itemPath));

if (status.getUntracked().contains(itemPath)) {
return UNTRACKED;
} else if (status.getAdded().contains(itemPath)) {
return ADDED;
} else if (status.getModified().contains(itemPath)
|| status.getChanged().contains(itemPath)) {
return MODIFIED;
} else {
return NOT_MODIFIED;
}
} catch (GitException | NotFoundException e) {
throw new ServerException(e.getMessage());
}
}

@Override
public Map<String, VcsStatus> getStatus(String wsPath, List<String> paths)
throws ServerException {
Map<String, VcsStatus> result = new HashMap<>();
try {
ProjectConfig project =
projectManager
.getClosest(absolutize(wsPath))
.orElseThrow(() -> new NotFoundException("Can't find project"));

Status status =
getStatus(
project.getName(), pathTransformer.transform(project.getPath()).toString(), paths);

paths.forEach(
path -> {
String itemWsPath = resolve(project.getPath(), path);
if (status.getUntracked().contains(path)) {
result.put(itemWsPath, UNTRACKED);
} else if (status.getAdded().contains(path)) {
result.put(itemWsPath, ADDED);
} else if (status.getModified().contains(path) || status.getChanged().contains(path)) {
result.put(itemWsPath, MODIFIED);
} else {
result.put(itemWsPath, NOT_MODIFIED);
}
});

} catch (NotFoundException e) {
throw new ServerException(e.getMessage());
}
return result;
}

private Status getStatus(String projectName, String projectFsPath, List<String> paths)
throws GitException {
if (statusCache.get(projectName) == null || haveChanges(projectFsPath, paths)) {
updateCachedStatus(
projectName, paths, gitConnectionFactory.getConnection(projectFsPath).status(paths));
}

return statusCache.get(projectName);
}

private boolean haveChanges(String projectPath, List<String> paths) {
boolean statusChanged = false;

for (String path : paths) {
String filePath = resolve(projectPath, path);
FileTime fileTime = projectFiles.get(filePath);
try {
FileTime currentFileTime = getLastModifiedTime(Paths.get(filePath));
if (fileTime == null || !fileTime.equals(currentFileTime)) {
projectFiles.put(filePath, currentFileTime);
statusChanged = true;
}
} catch (IOException exception) {
if (exception instanceof NoSuchFileException) {
statusChanged = projectFiles.remove(filePath) != null;
} else {
LOG.error(exception.getMessage());
}
}
}

return statusChanged;
}

private void updateCachedStatus(String project, List<String> paths, Status changes) {
Status cachedStatus = statusCache.get(project);

if (cachedStatus == null) {
statusCache.put(project, changes);
return;
}

paths.forEach(
path -> {
cachedStatus.getAdded().remove(path);
cachedStatus.getChanged().remove(path);
cachedStatus.getModified().remove(path);
cachedStatus.getUntracked().remove(path);
cachedStatus.getMissing().remove(path);
cachedStatus.getRemoved().remove(path);
cachedStatus.getConflicting().remove(path);
cachedStatus.getUntrackedFolders().remove(path);
});

changes.getAdded().forEach(added -> cachedStatus.getAdded().add(added));
changes.getChanged().forEach(changed -> cachedStatus.getChanged().add(changed));
changes.getModified().forEach(modified -> cachedStatus.getModified().add(modified));
changes.getUntracked().forEach(untracked -> cachedStatus.getUntracked().add(untracked));
changes.getMissing().forEach(missing -> cachedStatus.getMissing().add(missing));
changes.getRemoved().forEach(removed -> cachedStatus.getRemoved().add(removed));
changes.getConflicting().forEach(conflicting -> cachedStatus.getConflicting().add(conflicting));
changes
.getUntrackedFolders()
.forEach(untrackedFolders -> cachedStatus.getUntrackedFolders().add(untrackedFolders));

statusCache.put(project, cachedStatus);
}
}
Expand Up @@ -184,16 +184,18 @@ private Consumer<String> fsEventConsumer(String endpointId, String wsPath) {
fileStatus = NOT_MODIFIED;
}

FileChangedEventDto changedEventDto =
newDto(FileChangedEventDto.class)
.withPath(wsPath)
.withStatus(fileStatus)
.withEditedRegions(
fileStatus == MODIFIED ? gitConnection.getEditedRegions(itemPath) : null);
eventService.publish(changedEventDto);
transmitter
.newRequest()
.endpointId(endpointId)
.methodName(EVENT_GIT_FILE_CHANGED)
.paramsAsDto(
newDto(FileChangedEventDto.class)
.withPath(wsPath)
.withStatus(fileStatus)
.withEditedRegions(
fileStatus == MODIFIED ? gitConnection.getEditedRegions(itemPath) : null))
.paramsAsDto(changedEventDto)
.sendAndSkipResult();
} catch (GitCommitInProgressException | GitInvalidRepositoryException e) {
// Silent ignore
Expand Down
Expand Up @@ -38,7 +38,7 @@ protected void configure() {

Multibinder<VcsStatusProvider> vcsStatusProviderMultibinder =
newSetBinder(binder(), VcsStatusProvider.class);
vcsStatusProviderMultibinder.addBinding().to(GitStatusProvider.class);
vcsStatusProviderMultibinder.addBinding().to(CachedGitStatusProvider.class);

Multibinder<ValueProviderFactory> multiBinder =
Multibinder.newSetBinder(binder(), ValueProviderFactory.class);
Expand Down
Expand Up @@ -181,7 +181,7 @@ private Consumer<String> transmitConsumer(String wsPath) {
.withStatus(status)
.withModifiedFiles(modifiedFiles);

transmit(statusChangeEventDto, id);
eventService.publish(statusChangeEventDto);
} catch (GitCommitInProgressException
| GitCheckoutInProgressException
| GitInvalidRepositoryException e) {
Expand Down

0 comments on commit 30720b4

Please sign in to comment.