Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework GitStatusProvider to use cached status storage #10690

Merged
merged 5 commits into from Aug 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,294 @@
/*
* 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.annotation.PostConstruct;
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<>();
}

@PostConstruct
private void postConstruct() {
subscribeToEvents();
collectProjectFiles(rootDirPathProvider.get());
}

private void collectProjectFiles(String root) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose add @PostConstruct and remove call this method from constructor

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

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
Expand Up @@ -37,7 +37,9 @@
* Git implementation of {@link VcsStatusProvider}.
*
* @author Igor Vinokur
* @deprecated use {@link CachedGitStatusProvider}
*/
@Deprecated
public class GitStatusProvider implements VcsStatusProvider {

private final GitConnectionFactory gitConnectionFactory;
Expand Down