chore: in memory git status (#40891)

## Description
> [!TIP]  
> _Add a TL;DR when the description is longer than 500 words or
extremely technical (helps the content, marketing, and DevRel team)._
>
> _Please also include relevant motivation and context. List any
dependencies that are required for this change. Add links to Notion,
Figma or any other documents that might be relevant to the PR._


Fixes #`Issue Number`  
_or_  
Fixes `Issue URL`
> [!WARNING]  
> _If no issue exists, please create an issue first, and check with the
maintainers if the issue is valid._

## Automation

/ok-to-test tags="@tag.Git"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/15607977360>
> Commit: b50460f31752b3db4a31f035b19b1ea712c7fbde
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=15607977360&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Git`
> Spec:
> <hr>Thu, 12 Jun 2025 11:13:06 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced detailed Git status computation showing added, modified,
and removed files.
  - Added branch tracking status retrieval for repositories.

- **Improvements**
- Enhanced integration of Git status and branch tracking features across
interfaces and services.
- Improved status reporting by enriching Git status with branch tracking
details.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Diljit 2025-06-13 12:16:18 +05:30 committed by GitHub
parent 126f557605
commit dea1c030da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 152 additions and 11 deletions

View File

@ -1,5 +1,6 @@
package com.appsmith.git.files; package com.appsmith.git.files;
import com.appsmith.external.dtos.GitStatusDTO;
import com.appsmith.external.dtos.ModifiedResources; import com.appsmith.external.dtos.ModifiedResources;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
@ -19,6 +20,7 @@ import com.appsmith.git.configurations.GitServiceConfig;
import com.appsmith.git.constants.CommonConstants; import com.appsmith.git.constants.CommonConstants;
import com.appsmith.git.helpers.DSLTransformerHelper; import com.appsmith.git.helpers.DSLTransformerHelper;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.tracing.Span; import io.micrometer.tracing.Span;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -31,6 +33,7 @@ import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.FileSystemUtils; import org.springframework.util.FileSystemUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import reactor.core.observability.micrometer.Micrometer;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers; import reactor.core.scheduler.Schedulers;
@ -90,7 +93,7 @@ public class FileUtilsCEImpl implements FileInterface {
protected final FileOperations fileOperations; protected final FileOperations fileOperations;
private final ObservationHelper observationHelper; private final ObservationHelper observationHelper;
protected final ObjectMapper objectMapper; protected final ObjectMapper objectMapper;
private final ObservationRegistry observationRegistry;
private static final String EDIT_MODE_URL_TEMPLATE = "{{editModeUrl}}"; private static final String EDIT_MODE_URL_TEMPLATE = "{{editModeUrl}}";
private static final String VIEW_MODE_URL_TEMPLATE = "{{viewModeUrl}}"; private static final String VIEW_MODE_URL_TEMPLATE = "{{viewModeUrl}}";
@ -108,13 +111,15 @@ public class FileUtilsCEImpl implements FileInterface {
GitExecutor gitExecutor, GitExecutor gitExecutor,
FileOperations fileOperations, FileOperations fileOperations,
ObservationHelper observationHelper, ObservationHelper observationHelper,
ObjectMapper objectMapper) { ObjectMapper objectMapper,
ObservationRegistry observationRegistry) {
this.gitServiceConfig = gitServiceConfig; this.gitServiceConfig = gitServiceConfig;
this.fsGitHandler = fsGitHandler; this.fsGitHandler = fsGitHandler;
this.gitExecutor = gitExecutor; this.gitExecutor = gitExecutor;
this.fileOperations = fileOperations; this.fileOperations = fileOperations;
this.observationHelper = observationHelper; this.observationHelper = observationHelper;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.observationRegistry = observationRegistry;
} }
protected Map<GitResourceType, GitResourceType> getModifiedResourcesTypes() { protected Map<GitResourceType, GitResourceType> getModifiedResourcesTypes() {
@ -290,6 +295,66 @@ public class FileUtilsCEImpl implements FileInterface {
.subscribeOn(scheduler); .subscribeOn(scheduler);
} }
public Mono<GitStatusDTO> computeGitStatus(
Path baseRepoSuffix, GitResourceMap gitResourceMapFromDB, String branchName, boolean keepWorkingDirChanges)
throws GitAPIException, IOException {
return fsGitHandler
.resetToLastCommit(baseRepoSuffix, branchName, keepWorkingDirChanges)
.flatMap(__ -> constructGitResourceMapFromGitRepo(baseRepoSuffix, branchName))
.flatMap(gitResourceMapFromFS -> {
Map<GitResourceIdentity, Object> resourceMapFromDB = gitResourceMapFromDB.getGitResourceMap();
Map<GitResourceIdentity, Object> resourceMapFromFS = gitResourceMapFromFS.getGitResourceMap();
Map<String, Object> filePathObjectsMapFromFS = resourceMapFromFS.entrySet().parallelStream()
.collect(
Collectors.toMap(entry -> entry.getKey().getFilePath(), entry -> entry.getValue()));
Map<String, Object> filePathToObjectsFromDB = resourceMapFromDB.entrySet().parallelStream()
.collect(
Collectors.toMap(entry -> entry.getKey().getFilePath(), entry -> entry.getValue()));
Set<String> filePathsInDb = new HashSet<>(filePathToObjectsFromDB.keySet());
Set<String> filePathsInFS = new HashSet<>(filePathObjectsMapFromFS.keySet());
// added files
Set<String> addedFiles = new HashSet<>(filePathsInDb);
addedFiles.removeAll(filePathsInFS);
// removed files
Set<String> removedFiles = new HashSet<>(filePathsInFS);
removedFiles.removeAll(filePathsInDb);
removedFiles.remove(README_FILE_NAME);
// common files
Set<String> commonFiles = new HashSet<>(filePathsInDb);
commonFiles.retainAll(filePathsInFS);
// modified files
Set<String> modifiedFiles = commonFiles.stream()
.filter(filePath -> {
Object fileInDB = filePathToObjectsFromDB.get(filePath);
Object fileInFS = filePathObjectsMapFromFS.get(filePath);
try {
return fileOperations.hasFileChanged(fileInDB, fileInFS);
} catch (IOException e) {
log.error("Error while checking if file has changed", e);
return false;
}
})
.collect(Collectors.toSet());
GitStatusDTO localRepoStatus = new GitStatusDTO();
localRepoStatus.setAdded(addedFiles);
localRepoStatus.setModified(modifiedFiles);
localRepoStatus.setRemoved(removedFiles);
boolean isClean = addedFiles.isEmpty() && modifiedFiles.isEmpty() && removedFiles.isEmpty();
localRepoStatus.setIsClean(isClean);
fsGitHandler.populateModifiedEntities(localRepoStatus);
return Mono.just(localRepoStatus);
});
}
protected Set<String> getWhitelistedPaths() { protected Set<String> getWhitelistedPaths() {
String pages = PAGE_DIRECTORY + DELIMITER_PATH; String pages = PAGE_DIRECTORY + DELIMITER_PATH;
String datasources = DATASOURCE_DIRECTORY + DELIMITER_PATH; String datasources = DATASOURCE_DIRECTORY + DELIMITER_PATH;
@ -802,7 +867,10 @@ public class FileUtilsCEImpl implements FileInterface {
@Override @Override
public Mono<GitResourceMap> constructGitResourceMapFromGitRepo(Path repositorySuffix, String refName) { public Mono<GitResourceMap> constructGitResourceMapFromGitRepo(Path repositorySuffix, String refName) {
Path repositoryPath = Paths.get(gitServiceConfig.getGitRootPath()).resolve(repositorySuffix); Path repositoryPath = Paths.get(gitServiceConfig.getGitRootPath()).resolve(repositorySuffix);
return Mono.fromCallable(() -> fetchGitResourceMap(repositoryPath)).subscribeOn(scheduler); return Mono.fromCallable(() -> fetchGitResourceMap(repositoryPath))
.subscribeOn(scheduler)
.name("construct-git-resource-map")
.tap(Micrometer.observation(observationRegistry));
} }
/** /**

View File

@ -7,6 +7,7 @@ import com.appsmith.external.git.operations.FileOperations;
import com.appsmith.external.helpers.ObservationHelper; import com.appsmith.external.helpers.ObservationHelper;
import com.appsmith.git.configurations.GitServiceConfig; import com.appsmith.git.configurations.GitServiceConfig;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.micrometer.observation.ObservationRegistry;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
@ -26,7 +27,15 @@ public class FileUtilsImpl extends FileUtilsCEImpl implements FileInterface {
GitExecutor gitExecutor, GitExecutor gitExecutor,
FileOperations fileOperations, FileOperations fileOperations,
ObservationHelper observationHelper, ObservationHelper observationHelper,
ObjectMapper objectMapper) { ObjectMapper objectMapper,
super(gitServiceConfig, fsGitHandler, gitExecutor, fileOperations, observationHelper, objectMapper); ObservationRegistry observationRegistry) {
super(
gitServiceConfig,
fsGitHandler,
gitExecutor,
fileOperations,
observationHelper,
objectMapper,
observationRegistry);
} }
} }

View File

@ -935,7 +935,8 @@ public class FSGitHandlerCEImpl implements FSGitHandler {
.subscribeOn(scheduler); .subscribeOn(scheduler);
} }
protected void populateModifiedEntities(GitStatusDTO response) { @Override
public void populateModifiedEntities(GitStatusDTO response) {
populatePageChanges(response); populatePageChanges(response);
populateQueryChanges(response); populateQueryChanges(response);
populateJsObjectChanges(response); populateJsObjectChanges(response);

View File

@ -9,6 +9,7 @@ import com.appsmith.git.files.FileUtilsImpl;
import com.appsmith.git.files.operations.FileOperationsImpl; import com.appsmith.git.files.operations.FileOperationsImpl;
import com.appsmith.git.service.GitExecutorImpl; import com.appsmith.git.service.GitExecutorImpl;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.micrometer.observation.ObservationRegistry;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
@ -51,7 +52,8 @@ public class FileUtilsImplTest {
gitExecutor, gitExecutor,
fileOperations, fileOperations,
ObservationHelper.NOOP, ObservationHelper.NOOP,
new ObjectMapper()); new ObjectMapper(),
ObservationRegistry.NOOP);
} }
@AfterEach @AfterEach

View File

@ -1,5 +1,6 @@
package com.appsmith.external.git; package com.appsmith.external.git;
import com.appsmith.external.dtos.GitStatusDTO;
import com.appsmith.external.git.models.GitResourceMap; import com.appsmith.external.git.models.GitResourceMap;
import com.appsmith.external.models.ApplicationGitReference; import com.appsmith.external.models.ApplicationGitReference;
import com.appsmith.external.models.ArtifactGitReference; import com.appsmith.external.models.ArtifactGitReference;
@ -42,6 +43,10 @@ public interface FileInterface {
Path baseRepoSuffix, GitResourceMap gitResourceMap, String branchName, boolean keepWorkingDirChanges) Path baseRepoSuffix, GitResourceMap gitResourceMap, String branchName, boolean keepWorkingDirChanges)
throws GitAPIException, IOException; throws GitAPIException, IOException;
Mono<GitStatusDTO> computeGitStatus(
Path baseRepoSuffix, GitResourceMap gitResourceMap, String branchName, boolean keepWorkingDirChanges)
throws GitAPIException, IOException;
/** /**
* This method will reconstruct the application from the repo * This method will reconstruct the application from the repo
* *

View File

@ -160,6 +160,8 @@ public interface FSGitHandler {
*/ */
Mono<GitStatusDTO> getStatus(Path repoPath, String branchName, boolean keepWorkingDirChanges); Mono<GitStatusDTO> getStatus(Path repoPath, String branchName, boolean keepWorkingDirChanges);
void populateModifiedEntities(GitStatusDTO response);
/** /**
* This method merges source branch into destination branch for a git repository which is present on the partial * This method merges source branch into destination branch for a git repository which is present on the partial
* path provided. <B> This assumes that the branch on which the merge will happen is already checked out </B> * path provided. <B> This assumes that the branch on which the merge will happen is already checked out </B>

View File

@ -1675,8 +1675,10 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
Mono<GitStatusDTO> lockHandledStatusMono = Mono.usingWhen( Mono<GitStatusDTO> lockHandledStatusMono = Mono.usingWhen(
exportedArtifactJsonMono, exportedArtifactJsonMono,
artifactExchangeJson -> { artifactExchangeJson -> {
Mono<Boolean> prepareForStatus = Mono<GitStatusDTO> statusMono = gitHandlingService
gitHandlingService.prepareChangesToBeCommitted(jsonTransformationDTO, artifactExchangeJson); .computeGitStatus(jsonTransformationDTO, artifactExchangeJson)
.name("in-memory-status-computation")
.tap(Micrometer.observation(observationRegistry));
Mono<String> fetchRemoteMono = Mono.just("ignored"); Mono<String> fetchRemoteMono = Mono.just("ignored");
@ -1694,8 +1696,31 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
error.getMessage())))); error.getMessage()))));
} }
return Mono.zip(prepareForStatus, fetchRemoteMono) return Mono.zip(statusMono, fetchRemoteMono)
.then(Mono.defer(() -> gitHandlingService.getStatus(jsonTransformationDTO))) .flatMap(tuple -> {
return gitHandlingService
.getBranchTrackingStatus(jsonTransformationDTO)
.map(branchTrackingStatus -> {
GitStatusDTO status = tuple.getT1();
if (branchTrackingStatus != null) {
status.setAheadCount(branchTrackingStatus.getAheadCount());
status.setBehindCount(branchTrackingStatus.getBehindCount());
status.setRemoteBranch(branchTrackingStatus.getRemoteTrackingBranch());
} else {
log.debug(
"Remote tracking details not present for branch: {}, repo: {}",
finalBranchName,
repoName);
status.setAheadCount(0);
status.setBehindCount(0);
status.setRemoteBranch("untracked");
}
return status;
});
})
.onErrorResume(throwable -> { .onErrorResume(throwable -> {
/* /*
in case of any error, the global exception handler will release the lock in case of any error, the global exception handler will release the lock

View File

@ -86,6 +86,9 @@ public interface GitHandlingServiceCE {
Mono<Boolean> prepareChangesToBeCommitted( Mono<Boolean> prepareChangesToBeCommitted(
ArtifactJsonTransformationDTO jsonTransformationDTO, ArtifactExchangeJson artifactExchangeJson); ArtifactJsonTransformationDTO jsonTransformationDTO, ArtifactExchangeJson artifactExchangeJson);
Mono<GitStatusDTO> computeGitStatus(
ArtifactJsonTransformationDTO jsonTransformationDTO, ArtifactExchangeJson artifactExchangeJson);
Mono<Tuple2<? extends Artifact, String>> commitArtifact( Mono<Tuple2<? extends Artifact, String>> commitArtifact(
Artifact branchedArtifact, CommitDTO commitDTO, ArtifactJsonTransformationDTO jsonTransformationDTO); Artifact branchedArtifact, CommitDTO commitDTO, ArtifactJsonTransformationDTO jsonTransformationDTO);

View File

@ -764,6 +764,19 @@ public class GitFSServiceCEImpl implements GitHandlingServiceCE {
keepWorkingDirChanges -> fsGitHandler.getStatus(repoPath, refName, keepWorkingDirChanges)); keepWorkingDirChanges -> fsGitHandler.getStatus(repoPath, refName, keepWorkingDirChanges));
} }
public Mono<GitStatusDTO> computeGitStatus(
ArtifactJsonTransformationDTO jsonTransformationDTO, ArtifactExchangeJson artifactExchangeJson) {
String workspaceId = jsonTransformationDTO.getWorkspaceId();
String baseArtifactId = jsonTransformationDTO.getBaseArtifactId();
String repoName = jsonTransformationDTO.getRepoName();
String branchName = jsonTransformationDTO.getRefName();
ArtifactType artifactType = jsonTransformationDTO.getArtifactType();
GitArtifactHelper<?> gitArtifactHelper = gitArtifactHelperResolver.getArtifactHelper(artifactType);
Path repoSuffix = gitArtifactHelper.getRepoSuffixPath(workspaceId, baseArtifactId, repoName);
return commonGitFileUtils.computeGitStatus(repoSuffix, artifactExchangeJson, branchName);
}
@Override @Override
public Mono<String> createGitReference( public Mono<String> createGitReference(
ArtifactJsonTransformationDTO baseRefJsonTransformationDTO, ArtifactJsonTransformationDTO baseRefJsonTransformationDTO,

View File

@ -1,6 +1,7 @@
package com.appsmith.server.helpers.ce; package com.appsmith.server.helpers.ce;
import com.appsmith.external.constants.AnalyticsEvents; import com.appsmith.external.constants.AnalyticsEvents;
import com.appsmith.external.dtos.GitStatusDTO;
import com.appsmith.external.enums.FeatureFlagEnum; import com.appsmith.external.enums.FeatureFlagEnum;
import com.appsmith.external.git.FileInterface; import com.appsmith.external.git.FileInterface;
import com.appsmith.external.git.models.GitResourceIdentity; import com.appsmith.external.git.models.GitResourceIdentity;
@ -195,6 +196,18 @@ public class CommonGitFileUtilsCE {
}); });
} }
public Mono<GitStatusDTO> computeGitStatus(
Path baseRepoSuffix, ArtifactExchangeJson artifactExchangeJson, String branchName) {
GitResourceMap gitResourceMapFromDB = createGitResourceMap(artifactExchangeJson);
try {
return fileUtils
.computeGitStatus(baseRepoSuffix, gitResourceMapFromDB, branchName, true)
.subscribeOn(Schedulers.boundedElastic());
} catch (IOException | GitAPIException exception) {
return Mono.error(exception);
}
}
public Mono<Path> saveArtifactToLocalRepoWithAnalytics( public Mono<Path> saveArtifactToLocalRepoWithAnalytics(
Path baseRepoSuffix, ArtifactExchangeJson artifactExchangeJson, String branchName) { Path baseRepoSuffix, ArtifactExchangeJson artifactExchangeJson, String branchName) {