chore: git branches IT (#38805)

## 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, @tag.ImportExport"

### 🔍 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/12983176503>
> Commit: 946207867347024d59e989acbc6e019bdb3c263a
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=12983176503&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Git, @tag.ImportExport`
> Spec:
> <hr>Mon, 27 Jan 2025 07:09:02 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**
	- Added file format versioning for Git-related artifacts
	- Enhanced Git reference management with new methods and constants
	- Improved logging for Git operations

- **Bug Fixes**
	- Refined file path matching and handling in Git operations
	- Improved error handling during artifact and metadata processing

- **Refactor**
	- Centralized file and constant management across Git-related services
	- Updated method signatures to support more comprehensive Git workflows
	- Simplified code by removing hardcoded string literals

- **Tests**
- Added new integration test suite for Git branch and repository
operations
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Manish Kumar 2025-01-27 12:52:45 +05:30 committed by GitHub
parent a243e97510
commit e89788dd1a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 872 additions and 190 deletions

View File

@ -63,11 +63,13 @@ import java.util.stream.Stream;
import static com.appsmith.external.git.constants.GitConstants.ACTION_COLLECTION_LIST;
import static com.appsmith.external.git.constants.GitConstants.ACTION_LIST;
import static com.appsmith.external.git.constants.GitConstants.CUSTOM_JS_LIB_LIST;
import static com.appsmith.external.git.constants.GitConstants.GitMetricConstants.ACTION_COLLECTION_BODY;
import static com.appsmith.external.git.constants.GitConstants.GitMetricConstants.NEW_ACTION_BODY;
import static com.appsmith.external.git.constants.GitConstants.GitMetricConstants.RESOURCE_TYPE;
import static com.appsmith.external.git.constants.GitConstants.NAME_SEPARATOR;
import static com.appsmith.external.git.constants.GitConstants.PAGE_LIST;
import static com.appsmith.external.git.constants.ce.GitConstantsCE.GitMetricConstantsCE.ACTION_COLLECTION_BODY;
import static com.appsmith.external.git.constants.ce.GitConstantsCE.GitMetricConstantsCE.NEW_ACTION_BODY;
import static com.appsmith.external.git.constants.ce.GitConstantsCE.GitMetricConstantsCE.RESOURCE_TYPE;
import static com.appsmith.external.git.constants.GitConstants.README_FILE_NAME;
import static com.appsmith.git.constants.CommonConstants.JSON_EXTENSION;
import static com.appsmith.git.constants.GitDirectories.ACTION_COLLECTION_DIRECTORY;
import static com.appsmith.git.constants.GitDirectories.ACTION_DIRECTORY;
import static com.appsmith.git.constants.GitDirectories.DATASOURCE_DIRECTORY;
@ -265,7 +267,8 @@ public class FileUtilsCEImpl implements FileInterface {
try (Stream<Path> stream = Files.walk(baseRepo).parallel()) {
return stream.filter(path -> {
try {
return Files.isRegularFile(path) || FileUtils.isEmptyDirectory(path.toFile());
return !path.toString().contains(".git" + File.separator)
&& (Files.isRegularFile(path) || FileUtils.isEmptyDirectory(path.toFile()));
} catch (IOException e) {
log.error("Unable to find file details. Please check the file at file path: {}", path);
log.error("Assuming that it does not exist for now ...");
@ -291,7 +294,7 @@ public class FileUtilsCEImpl implements FileInterface {
// Remove all files that need to be serialized from the existing files list, as well as the README file
// What we are left with are all the files to be deleted
filesInRepo.removeAll(updatedFilesToBeSerialized);
filesInRepo.remove("README.md");
filesInRepo.remove(README_FILE_NAME);
// Delete all the files because they are no longer needed
// This covers both older structures of storing files and,
@ -345,15 +348,13 @@ public class FileUtilsCEImpl implements FileInterface {
// Save application
saveResource(
applicationGitReference.getApplication(),
baseRepo.resolve(CommonConstants.APPLICATION + CommonConstants.JSON_EXTENSION));
baseRepo.resolve(CommonConstants.APPLICATION + JSON_EXTENSION));
// Save application metadata
fileOperations.saveMetadataResource(applicationGitReference, baseRepo);
// Save application theme
saveResource(
applicationGitReference.getTheme(),
baseRepo.resolve(CommonConstants.THEME + CommonConstants.JSON_EXTENSION));
saveResource(applicationGitReference.getTheme(), baseRepo.resolve(CommonConstants.THEME + JSON_EXTENSION));
// Save pages
Path pageDirectory = baseRepo.resolve(PAGE_DIRECTORY);
@ -369,9 +370,7 @@ public class FileUtilsCEImpl implements FileInterface {
modifiedResources != null && modifiedResources.isResourceUpdated(PAGE_LIST, pageName);
if (Boolean.TRUE.equals(isResourceUpdated)) {
// Save page metadata
saveResource(
pageResource.getValue(),
pageSpecificDirectory.resolve(pageName + CommonConstants.JSON_EXTENSION));
saveResource(pageResource.getValue(), pageSpecificDirectory.resolve(pageName + JSON_EXTENSION));
Map<String, JSONObject> result = DSLTransformerHelper.flatten(
new JSONObject(applicationGitReference.getPageDsl().get(pageName)));
result.keySet().parallelStream().forEach(key -> {
@ -390,8 +389,7 @@ public class FileUtilsCEImpl implements FileInterface {
pageSpecificDirectory.resolve(CommonConstants.WIDGETS).toFile(), validWidgetToParentMap);
// Remove the canvas.json from the file system since the value is stored in the page.json
fileOperations.deleteFile(
pageSpecificDirectory.resolve(CommonConstants.CANVAS + CommonConstants.JSON_EXTENSION));
fileOperations.deleteFile(pageSpecificDirectory.resolve(CommonConstants.CANVAS + JSON_EXTENSION));
}
validPages.add(pageName);
}
@ -416,7 +414,7 @@ public class FileUtilsCEImpl implements FileInterface {
String uidString = jsLibEntry.getKey();
boolean isResourceUpdated = modifiedResources.isResourceUpdated(CUSTOM_JS_LIB_LIST, uidString);
String fileNameWithExtension = getJsLibFileName(uidString) + CommonConstants.JSON_EXTENSION;
String fileNameWithExtension = getJsLibFileName(uidString) + JSON_EXTENSION;
Path jsLibSpecificFile = jsLibDirectory.resolve(fileNameWithExtension);
if (isResourceUpdated) {
@ -463,9 +461,8 @@ public class FileUtilsCEImpl implements FileInterface {
queryName,
actionSpecificDirectory.resolve(queryName));
// Delete the resource from the old file structure v2
fileOperations.deleteFile(pageSpecificDirectory
.resolve(ACTION_DIRECTORY)
.resolve(queryName + CommonConstants.JSON_EXTENSION));
fileOperations.deleteFile(
pageSpecificDirectory.resolve(ACTION_DIRECTORY).resolve(queryName + JSON_EXTENSION));
}
}
});
@ -504,8 +501,8 @@ public class FileUtilsCEImpl implements FileInterface {
actionCollectionName,
actionCollectionSpecificDirectory.resolve(actionCollectionName));
// Delete the resource from the old file structure v2
fileOperations.deleteFile(actionCollectionSpecificDirectory.resolve(
actionCollectionName + CommonConstants.JSON_EXTENSION));
fileOperations.deleteFile(
actionCollectionSpecificDirectory.resolve(actionCollectionName + JSON_EXTENSION));
}
}
});
@ -522,8 +519,8 @@ public class FileUtilsCEImpl implements FileInterface {
applicationGitReference.getDatasources().entrySet()) {
saveResource(
resource.getValue(),
baseRepo.resolve(DATASOURCE_DIRECTORY).resolve(resource.getKey() + CommonConstants.JSON_EXTENSION));
validDatasourceFileNames.add(resource.getKey() + CommonConstants.JSON_EXTENSION);
baseRepo.resolve(DATASOURCE_DIRECTORY).resolve(resource.getKey() + JSON_EXTENSION));
validDatasourceFileNames.add(resource.getKey() + JSON_EXTENSION);
}
// Scan datasource directory and delete any unwanted files if present
if (!applicationGitReference.getDatasources().isEmpty()) {
@ -594,7 +591,7 @@ public class FileUtilsCEImpl implements FileInterface {
}
// Write metadata for the jsObject
Path metadataPath = path.resolve(CommonConstants.METADATA + CommonConstants.JSON_EXTENSION);
Path metadataPath = path.resolve(CommonConstants.METADATA + JSON_EXTENSION);
return fileOperations.writeToFile(sourceEntity, metadataPath);
} catch (IOException e) {
log.debug(e.getMessage());
@ -630,7 +627,7 @@ public class FileUtilsCEImpl implements FileInterface {
}
// Write metadata for the actions
Path metadataPath = path.resolve(CommonConstants.METADATA + CommonConstants.JSON_EXTENSION);
Path metadataPath = path.resolve(CommonConstants.METADATA + JSON_EXTENSION);
return fileOperations.writeToFile(sourceEntity, metadataPath);
} catch (IOException e) {
log.error("Error while reading file {} with message {} with cause", path, e.getMessage(), e.getCause());
@ -769,9 +766,8 @@ public class FileUtilsCEImpl implements FileInterface {
if (resourcePath.toFile().exists()) {
body = fileOperations.readFileAsString(resourcePath);
}
Object file = fileOperations.readFile(directoryPath
.resolve(resourceName)
.resolve(CommonConstants.METADATA + CommonConstants.JSON_EXTENSION));
Object file = fileOperations.readFile(
directoryPath.resolve(resourceName).resolve(CommonConstants.METADATA + JSON_EXTENSION));
actionCollectionBodyMap.put(resourceName + keySuffix, body);
resource.put(resourceName + keySuffix, file);
}
@ -799,9 +795,8 @@ public class FileUtilsCEImpl implements FileInterface {
if (queryPath.toFile().exists()) {
body = fileOperations.readFileAsString(queryPath);
}
Object file = fileOperations.readFile(directoryPath
.resolve(resourceName)
.resolve(CommonConstants.METADATA + CommonConstants.JSON_EXTENSION));
Object file = fileOperations.readFile(
directoryPath.resolve(resourceName).resolve(CommonConstants.METADATA + JSON_EXTENSION));
actionCollectionBodyMap.put(resourceName + keySuffix, body);
resource.put(resourceName + keySuffix, file);
}
@ -811,13 +806,12 @@ public class FileUtilsCEImpl implements FileInterface {
private Object readPageMetadata(Path directoryPath) {
return fileOperations.readFile(
directoryPath.resolve(directoryPath.toFile().getName() + CommonConstants.JSON_EXTENSION));
directoryPath.resolve(directoryPath.toFile().getName() + JSON_EXTENSION));
}
protected GitResourceMap fetchGitResourceMap(Path baseRepoPath) throws IOException {
// Extract application metadata from the json
Object metadata = fileOperations.readFile(
baseRepoPath.resolve(CommonConstants.METADATA + CommonConstants.JSON_EXTENSION));
Object metadata = fileOperations.readFile(baseRepoPath.resolve(CommonConstants.METADATA + JSON_EXTENSION));
Integer fileFormatVersion = fileOperations.getFileFormatVersion(metadata);
// Check if fileFormat of the saved files in repo is compatible
if (!isFileFormatCompatible(fileFormatVersion)) {
@ -828,6 +822,9 @@ public class FileUtilsCEImpl implements FileInterface {
Map<GitResourceIdentity, Object> resourceMap = gitResourceMap.getGitResourceMap();
Set<String> filesInRepo = getExistingFilesInRepo(baseRepoPath);
// Remove all files that need not be fetched to the git resource map
// i.e. -> README.md
filesInRepo.remove(README_FILE_NAME);
filesInRepo.parallelStream()
.filter(path -> !Files.isDirectory(baseRepoPath.resolve(path)))
@ -853,7 +850,7 @@ public class FileUtilsCEImpl implements FileInterface {
} else if (filePath.matches(JS_LIB_DIRECTORY + "/.*")) {
String fileName = FilenameUtils.getBaseName(filePath);
identity = new GitResourceIdentity(GitResourceType.JSLIB_CONFIG, fileName, filePath);
} else if (filePath.matches(PAGE_DIRECTORY + "/[^/]*/[^/]*.json]")) {
} else if (filePath.matches(PAGE_DIRECTORY + "/[^/]*/[^/]*.json")) {
String gitSyncId =
objectMapper.valueToTree(contents).get("gitSyncId").asText();
identity = new GitResourceIdentity(GitResourceType.CONTEXT_CONFIG, gitSyncId, filePath);
@ -866,6 +863,7 @@ public class FileUtilsCEImpl implements FileInterface {
String gitSyncId =
objectMapper.valueToTree(configContents).get("gitSyncId").asText();
identity = new GitResourceIdentity(GitResourceType.QUERY_DATA, gitSyncId, filePath);
contents = fileOperations.readFileAsString(path);
} else if (filePath.matches(PAGE_DIRECTORY + "/[^/]*/" + ACTION_COLLECTION_DIRECTORY + "/.*/metadata.json")) {
String gitSyncId =
objectMapper.valueToTree(contents).get("gitSyncId").asText();
@ -875,6 +873,7 @@ public class FileUtilsCEImpl implements FileInterface {
String gitSyncId =
objectMapper.valueToTree(configContents).get("gitSyncId").asText();
identity = new GitResourceIdentity(GitResourceType.JSOBJECT_DATA, gitSyncId, filePath);
contents = fileOperations.readFileAsString(path);
} else if (filePath.matches(PAGE_DIRECTORY + "/[^/]*/widgets/.*\\.json")) {
Pattern pageDirPattern = Pattern.compile("(" + PAGE_DIRECTORY + "/([^/]*))/widgets/.*\\.json");
Matcher matcher = pageDirPattern.matcher(filePath);
@ -886,7 +885,7 @@ public class FileUtilsCEImpl implements FileInterface {
String gitSyncId =
objectMapper.valueToTree(configContents).get("gitSyncId").asText();
String widgetId = objectMapper.valueToTree(contents).get("widgetId").asText();
identity = new GitResourceIdentity(GitResourceType.JSOBJECT_DATA, gitSyncId + "-" + widgetId, filePath);
identity = new GitResourceIdentity(GitResourceType.WIDGET_CONFIG, gitSyncId + "-" + widgetId, filePath);
} else return null;
return Tuples.of(identity, contents);
@ -895,18 +894,17 @@ public class FileUtilsCEImpl implements FileInterface {
private ApplicationGitReference fetchApplicationReference(Path baseRepoPath) {
ApplicationGitReference applicationGitReference = new ApplicationGitReference();
// Extract application metadata from the json
Object metadata = fileOperations.readFile(
baseRepoPath.resolve(CommonConstants.METADATA + CommonConstants.JSON_EXTENSION));
Object metadata = fileOperations.readFile(baseRepoPath.resolve(CommonConstants.METADATA + JSON_EXTENSION));
Integer fileFormatVersion = fileOperations.getFileFormatVersion(metadata);
// Check if fileFormat of the saved files in repo is compatible
if (!isFileFormatCompatible(fileFormatVersion)) {
throw new AppsmithPluginException(AppsmithPluginError.INCOMPATIBLE_FILE_FORMAT);
}
// Extract application data from the json
applicationGitReference.setApplication(fileOperations.readFile(
baseRepoPath.resolve(CommonConstants.APPLICATION + CommonConstants.JSON_EXTENSION)));
applicationGitReference.setApplication(
fileOperations.readFile(baseRepoPath.resolve(CommonConstants.APPLICATION + JSON_EXTENSION)));
applicationGitReference.setTheme(
fileOperations.readFile(baseRepoPath.resolve(CommonConstants.THEME + CommonConstants.JSON_EXTENSION)));
fileOperations.readFile(baseRepoPath.resolve(CommonConstants.THEME + JSON_EXTENSION)));
Path pageDirectory = baseRepoPath.resolve(PAGE_DIRECTORY);
// Reconstruct application from given file format
switch (fileFormatVersion) {
@ -965,8 +963,7 @@ public class FileUtilsCEImpl implements FileInterface {
for (File page : Objects.requireNonNull(directory.listFiles())) {
pageMap.put(
page.getName(),
fileOperations.readFile(
page.toPath().resolve(CommonConstants.CANVAS + CommonConstants.JSON_EXTENSION)));
fileOperations.readFile(page.toPath().resolve(CommonConstants.CANVAS + JSON_EXTENSION)));
if (fileFormatVersion >= 4) {
actionMap.putAll(
@ -1107,7 +1104,7 @@ public class FileUtilsCEImpl implements FileInterface {
deleteWidgets(file, validWidgetToParentMap);
}
String name = file.getName().replace(CommonConstants.JSON_EXTENSION, CommonConstants.EMPTY_STRING);
String name = file.getName().replace(JSON_EXTENSION, CommonConstants.EMPTY_STRING);
// If input widget was inside a container before, but the user moved it out of the container
// then we need to delete the widget from the container directory
// The check here is to validate if the parent is correct or not
@ -1192,8 +1189,8 @@ public class FileUtilsCEImpl implements FileInterface {
metadataMono = gitResetMono.map(isSwitched -> {
Path baseRepoPath = Paths.get(gitServiceConfig.getGitRootPath()).resolve(baseRepoSuffix);
Object metadata = fileOperations.readFile(
baseRepoPath.resolve(CommonConstants.METADATA + CommonConstants.JSON_EXTENSION));
Object metadata =
fileOperations.readFile(baseRepoPath.resolve(CommonConstants.METADATA + JSON_EXTENSION));
return metadata;
});
} catch (GitAPIException | IOException exception) {
@ -1222,8 +1219,7 @@ public class FileUtilsCEImpl implements FileInterface {
.resolve(baseRepoSuffixPath)
.resolve(pageSuffix);
Object pageObject =
fileOperations.readFile(repoPath.resolve(pageName + CommonConstants.JSON_EXTENSION));
Object pageObject = fileOperations.readFile(repoPath.resolve(pageName + JSON_EXTENSION));
return pageObject;
});

View File

@ -167,9 +167,9 @@ public class FileOperationsCEv2Impl implements FileOperationsCE {
if (metadata == null) {
return 1;
}
JsonNode json = objectMapper.valueToTree(metadata);
int fileFormatVersion = json.get(CommonConstants.FILE_FORMAT_VERSION).asInt();
return fileFormatVersion;
return json.get(CommonConstants.FILE_FORMAT_VERSION).asInt();
}
@Override

View File

@ -436,12 +436,13 @@ public class FSGitHandlerCEImpl implements FSGitHandler {
return Mono.using(
() -> Git.open(createRepoPath(repoSuffix).toFile()),
git -> Mono.fromCallable(() -> {
log.debug(Thread.currentThread().getName() + ": Switching to the branch "
+ branchName);
log.info(
"{}: Switching to the branch {}",
Thread.currentThread().getName(),
branchName);
// We can safely assume that repo has been already initialised either in commit or
// clone flow and
// can directly
// open the repo
// clone flow and can directly open the repo
if (StringUtils.equalsIgnoreCase(
branchName, git.getRepository().getBranch())) {
return TRUE;
@ -611,8 +612,12 @@ public class FSGitHandlerCEImpl implements FSGitHandler {
return Mono.using(
() -> Git.open(repoPath.toFile()),
git -> Mono.fromCallable(() -> {
log.debug(Thread.currentThread().getName() + ": Get status for repo " + repoPath
+ ", branch " + branchName);
log.info(
"{}: Get status for repo {}, {}",
Thread.currentThread().getName(),
repoPath,
branchName);
Status status = git.status().call();
GitStatusDTO response = new GitStatusDTO();

View File

@ -9,6 +9,8 @@ public class GitConstantsCE {
public static final String ACTION_LIST = "actionList";
public static final String ACTION_COLLECTION_LIST = "actionCollectionList";
public static final String README_FILE_NAME = "README.md";
public static final String DEFAULT_COMMIT_MESSAGE = "System generated commit, ";
public static final String EMPTY_COMMIT_ERROR_MESSAGE = "On current branch nothing to commit, working tree clean";
public static final String MERGE_CONFLICT_BRANCH_NAME = "_mergeConflict";

View File

@ -67,10 +67,12 @@ import static com.appsmith.external.git.constants.GitConstants.NAME_SEPARATOR;
import static com.appsmith.external.helpers.AppsmithBeanUtils.copyNestedNonNullProperties;
import static com.appsmith.external.helpers.AppsmithBeanUtils.copyProperties;
import static com.appsmith.git.constants.CommonConstants.DELIMITER_PATH;
import static com.appsmith.git.constants.CommonConstants.FILE_FORMAT_VERSION;
import static com.appsmith.git.constants.CommonConstants.JSON_EXTENSION;
import static com.appsmith.git.constants.CommonConstants.MAIN_CONTAINER;
import static com.appsmith.git.constants.CommonConstants.WIDGETS;
import static com.appsmith.git.constants.ce.GitDirectoriesCE.PAGE_DIRECTORY;
import static com.appsmith.git.constants.CommonConstants.fileFormatVersion;
import static com.appsmith.git.constants.GitDirectories.PAGE_DIRECTORY;
import static com.appsmith.server.constants.FieldName.ACTION_COLLECTION_LIST;
import static com.appsmith.server.constants.FieldName.ACTION_LIST;
import static com.appsmith.server.constants.FieldName.CHILDREN;
@ -189,6 +191,10 @@ public class ApplicationGitFileUtilsCEImpl implements ArtifactGitFileUtilsCE<App
copyProperties(applicationJson, applicationMetadata, keys);
final String metadataFilePath = CommonConstants.METADATA + JSON_EXTENSION;
ObjectNode metadata = objectMapper.valueToTree(applicationMetadata);
// put file format version;
metadata.put(FILE_FORMAT_VERSION, fileFormatVersion);
GitResourceIdentity metadataIdentity =
new GitResourceIdentity(GitResourceType.ROOT_CONFIG, metadataFilePath, metadataFilePath);
resourceMap.put(metadataIdentity, metadata);
@ -717,7 +723,6 @@ public class ApplicationGitFileUtilsCEImpl implements ArtifactGitFileUtilsCE<App
artifactExchangeJson.setContextList(pageList);
// widgets
pageList.parallelStream().forEach(newPage -> {
Map<String, org.json.JSONObject> widgetsData = resourceMap.entrySet().stream()
.filter(entry -> {
@ -730,13 +735,23 @@ public class ApplicationGitFileUtilsCEImpl implements ArtifactGitFileUtilsCE<App
.getFilePath()
.replaceFirst(
PAGE_DIRECTORY
+ DELIMITER_PATH
+ newPage.getUnpublishedPage()
.getName()
+ DELIMITER_PATH
+ WIDGETS
+ DELIMITER_PATH,
MAIN_CONTAINER + DELIMITER_PATH),
entry -> (org.json.JSONObject) entry.getValue()));
entry -> {
try {
return new org.json.JSONObject(objectMapper.writeValueAsString(entry.getValue()));
} catch (JsonProcessingException jsonProcessingException) {
log.error(
"Error while deserializing widget with file path {}",
entry.getKey().getFilePath());
throw new RuntimeException(jsonProcessingException);
}
}));
Layout layout = newPage.getUnpublishedPage().getLayouts().get(0);
org.json.JSONObject mainContainer;

View File

@ -8,6 +8,7 @@ import com.appsmith.external.dtos.GitStatusDTO;
import com.appsmith.external.dtos.MergeStatusDTO;
import com.appsmith.external.git.constants.GitConstants;
import com.appsmith.external.git.constants.GitSpan;
import com.appsmith.external.git.constants.ce.GitConstantsCE;
import com.appsmith.external.git.constants.ce.RefType;
import com.appsmith.external.git.dtos.FetchRemoteDTO;
import com.appsmith.external.models.Datasource;
@ -78,7 +79,6 @@ import reactor.util.function.Tuple2;
import reactor.util.function.Tuple3;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
@ -95,9 +95,9 @@ import static com.appsmith.external.git.constants.ce.GitSpanCE.OPS_STATUS;
import static com.appsmith.external.helpers.AppsmithBeanUtils.copyNestedNonNullProperties;
import static com.appsmith.server.constants.FieldName.BRANCH_NAME;
import static com.appsmith.server.constants.FieldName.DEFAULT;
import static com.appsmith.server.constants.FieldName.REF_NAME;
import static com.appsmith.server.constants.FieldName.REF_TYPE;
import static com.appsmith.server.constants.SerialiseArtifactObjective.VERSION_CONTROL;
import static com.appsmith.server.constants.ce.FieldNameCE.REF_NAME;
import static com.appsmith.server.constants.ce.FieldNameCE.REF_TYPE;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static org.springframework.util.StringUtils.hasText;
@ -510,13 +510,13 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
final String workspaceId = baseArtifact.getWorkspaceId();
final String finalRemoteRefName = gitRefDTO.getRefName().replaceFirst(ORIGIN, REMOTE_NAME_REPLACEMENT);
ArtifactJsonTransformationDTO jsonTransformationDTO = new ArtifactJsonTransformationDTO();
jsonTransformationDTO.setRepoName(repoName);
ArtifactJsonTransformationDTO jsonTransformationDTO = new ArtifactJsonTransformationDTO(
workspaceId, baseArtifactId, repoName, baseArtifact.getArtifactType());
jsonTransformationDTO.setRefType(gitRefDTO.getRefType());
jsonTransformationDTO.setRefName(finalRemoteRefName);
jsonTransformationDTO.setWorkspaceId(workspaceId);
jsonTransformationDTO.setBaseArtifactId(baseArtifactId);
jsonTransformationDTO.setArtifactType(baseArtifact.getArtifactType());
FetchRemoteDTO fetchRemoteDTO = new FetchRemoteDTO(List.of(finalRemoteRefName), gitRefDTO.getRefType(), false);
Mono<? extends Artifact> artifactMono;
if (baseRefName.equals(finalRemoteRefName)) {
@ -528,11 +528,12 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
artifactMono = Mono.just(baseArtifact);
} else {
// create new Artifact
artifactMono = gitArtifactHelper.createNewArtifactForCheckout(baseArtifact, finalRemoteRefName);
artifactMono = generateArtifactForRefCreation(baseArtifact, finalRemoteRefName, gitRefDTO.getRefType());
}
Mono<? extends Artifact> checkedOutRemoteArtifactMono = gitHandlingService
.fetchRemoteReferences(jsonTransformationDTO, baseGitMetadata.getGitAuth(), false)
.fetchRemoteReferences(jsonTransformationDTO, fetchRemoteDTO, baseGitMetadata.getGitAuth())
.flatMap(ignoredFetchString -> gitHandlingService.checkoutRemoteReference(jsonTransformationDTO))
.onErrorResume(error -> Mono.error(
new AppsmithException(AppsmithError.GIT_ACTION_FAILED, "checkout branch", error.getMessage())))
.flatMap(ignoreRemoteChanges -> {
@ -600,18 +601,6 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
GitArtifactMetadata baseGitMetadata = baseArtifact.getGitArtifactMetadata();
GitAuth baseGitAuth = baseGitMetadata.getGitAuth();
GitArtifactMetadata sourceGitMetadata = sourceArtifact.getGitArtifactMetadata();
ArtifactType artifactType = baseArtifact.getArtifactType();
GitArtifactHelper<?> gitArtifactHelper = gitArtifactHelperResolver.getArtifactHelper(artifactType);
GitHandlingService gitHandlingService = gitHandlingServiceResolver.getGitHandlingService(gitType);
ArtifactJsonTransformationDTO jsonTransformationDTO = new ArtifactJsonTransformationDTO();
jsonTransformationDTO.setWorkspaceId(baseArtifact.getWorkspaceId());
jsonTransformationDTO.setBaseArtifactId(baseGitMetadata.getDefaultArtifactId());
jsonTransformationDTO.setRepoName(baseGitMetadata.getRepoName());
jsonTransformationDTO.setArtifactType(baseArtifact.getArtifactType());
jsonTransformationDTO.setRefType(refType);
jsonTransformationDTO.setRefName(refDTO.getRefName());
if (sourceGitMetadata == null
|| !hasText(sourceGitMetadata.getDefaultArtifactId())
@ -622,19 +611,40 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
"Unable to find the parent reference. Please create a reference from other available references"));
}
ArtifactType artifactType = baseArtifact.getArtifactType();
String workspaceId = baseArtifact.getWorkspaceId();
String baseArtifactId = baseGitMetadata.getDefaultArtifactId();
String repoName = baseGitMetadata.getRepoName();
String sourceArtifactId = sourceArtifact.getId();
GitArtifactHelper<?> gitArtifactHelper = gitArtifactHelperResolver.getArtifactHelper(artifactType);
GitHandlingService gitHandlingService = gitHandlingServiceResolver.getGitHandlingService(gitType);
ArtifactJsonTransformationDTO baseRefTransformationDTO =
new ArtifactJsonTransformationDTO(workspaceId, baseArtifactId, repoName, artifactType);
baseRefTransformationDTO.setRefName(sourceGitMetadata.getRefName());
baseRefTransformationDTO.setRefType(refType);
ArtifactJsonTransformationDTO createRefTransformationDTO =
new ArtifactJsonTransformationDTO(workspaceId, baseArtifactId, repoName, artifactType);
createRefTransformationDTO.setRefType(refType);
createRefTransformationDTO.setRefName(refDTO.getRefName());
Mono<Boolean> acquireGitLockMono = gitRedisUtils.acquireGitLock(
artifactType,
baseGitMetadata.getDefaultArtifactId(),
GitConstants.GitCommandConstants.CREATE_BRANCH,
FALSE);
Mono<String> fetchRemoteMono =
gitHandlingService.fetchRemoteReferences(jsonTransformationDTO, baseGitAuth, TRUE);
gitHandlingService.fetchRemoteReferences(baseRefTransformationDTO, baseGitAuth, TRUE);
Mono<? extends Artifact> createBranchMono = acquireGitLockMono
.flatMap(ignoreLockAcquisition -> fetchRemoteMono.onErrorResume(
error -> Mono.error(new AppsmithException(AppsmithError.GIT_ACTION_FAILED, "fetch", error))))
.flatMap(ignoreFetchString -> gitHandlingService
.listReferences(jsonTransformationDTO, TRUE)
.listReferences(createRefTransformationDTO, TRUE)
.flatMap(refList -> {
boolean isDuplicateName = refList.stream()
// We are only supporting origin as the remote name so this is safe
@ -655,11 +665,10 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
Mono<? extends ArtifactExchangeJson> artifactExchangeJsonMono =
exportService.exportByArtifactId(
sourceArtifact.getId(), VERSION_CONTROL, baseArtifact.getArtifactType());
sourceArtifactId, VERSION_CONTROL, baseArtifact.getArtifactType());
Mono<? extends Artifact> newArtifactFromSourceMono =
// TODO: add refType support over here
gitArtifactHelper.createNewArtifactForCheckout(sourceArtifact, refDTO.getRefName());
Mono<? extends Artifact> newArtifactFromSourceMono = generateArtifactForRefCreation(
sourceArtifact, refDTO.getRefName(), refDTO.getRefType());
return refCreationValidationMono.flatMap(isOkayToProceed -> {
if (!TRUE.equals(isOkayToProceed)) {
@ -675,7 +684,7 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
Artifact newRefArtifact = tuple.getT1();
Mono<String> refCreationMono = gitHandlingService
.createGitReference(jsonTransformationDTO, refDTO)
.createGitReference(createRefTransformationDTO, baseGitMetadata, refDTO)
// TODO: this error could be shipped to handling layer as well?
.onErrorResume(error -> Mono.error(new AppsmithException(
AppsmithError.GIT_ACTION_FAILED, "ref creation preparation", error.getMessage())));
@ -694,13 +703,11 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
})
// after the branch is created, we need to reset the older branch to the
// clean status, i.e. last commit
.flatMap(newImportedArtifact -> discardChanges(sourceArtifact, gitType));
.flatMap(newImportedArtifact ->
discardChanges(sourceArtifact, gitType).thenReturn(newImportedArtifact));
})
.flatMap(newImportedArtifact -> gitRedisUtils
.releaseFileLock(
artifactType,
newImportedArtifact.getGitArtifactMetadata().getDefaultArtifactId(),
TRUE)
.releaseFileLock(artifactType, baseArtifactId, TRUE)
.then(gitAnalyticsUtils.addAnalyticsForGitOperation(
AnalyticsEvents.GIT_CREATE_BRANCH,
newImportedArtifact,
@ -708,7 +715,7 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
.onErrorResume(error -> {
log.error("An error occurred while creating reference. error {}", error.getMessage());
return gitRedisUtils
.releaseFileLock(artifactType, baseGitMetadata.getDefaultArtifactId(), TRUE)
.releaseFileLock(artifactType, baseArtifactId, TRUE)
.then(Mono.error(new AppsmithException(AppsmithError.GIT_ACTION_FAILED, "checkout")));
})
.name(GitSpan.OPS_CREATE_BRANCH)
@ -1066,7 +1073,7 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
jsonTransformationDTO.setArtifactType(artifactType);
jsonTransformationDTO.setRepoName(repoName);
final String README_FILE_NAME = "README.md";
final String README_FILE_NAME = GitConstantsCE.README_FILE_NAME;
Mono<Boolean> readMeIntialisationMono = gitHandlingService.initialiseReadMe(
jsonTransformationDTO, artifact, README_FILE_NAME, originHeader);
@ -1095,30 +1102,31 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
commitDTO.setMessage(commitMessage);
return this.commitArtifact(commitDTO, artifact.getId(), artifactType, gitType)
.onErrorResume(error ->
// If the push fails remove all the cloned files from local repo
this.detachRemote(baseArtifactId, artifactType, gitType)
.flatMap(isDeleted -> {
if (error instanceof TransportException) {
return gitAnalyticsUtils
.addAnalyticsForGitOperation(
AnalyticsEvents.GIT_CONNECT,
artifact,
error.getClass()
.getName(),
error.getMessage(),
artifact.getGitArtifactMetadata()
.getIsRepoPrivate())
.then(Mono.error(new AppsmithException(
AppsmithError
.INVALID_GIT_SSH_CONFIGURATION,
error.getMessage())));
}
return Mono.error(new AppsmithException(
AppsmithError.GIT_ACTION_FAILED,
"push",
error.getMessage()));
}));
.onErrorResume(error -> {
log.error("Error while committing", error);
// If the push fails remove all the cloned files from local repo
return this.detachRemote(baseArtifactId, artifactType, gitType)
.flatMap(isDeleted -> {
if (error instanceof TransportException) {
return gitAnalyticsUtils
.addAnalyticsForGitOperation(
AnalyticsEvents.GIT_CONNECT,
artifact,
error.getClass()
.getName(),
error.getMessage(),
artifact.getGitArtifactMetadata()
.getIsRepoPrivate())
.then(Mono.error(new AppsmithException(
AppsmithError.INVALID_GIT_SSH_CONFIGURATION,
error.getMessage())));
}
return Mono.error(new AppsmithException(
AppsmithError.GIT_ACTION_FAILED,
"push",
error.getMessage()));
});
});
})
.then(gitAnalyticsUtils.addAnalyticsForGitOperation(
AnalyticsEvents.GIT_CONNECT,
@ -1429,23 +1437,26 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
jsonTransformationDTO.setRefName(gitArtifactMetadata.getRefName());
// Remove the git contents from file system
return Mono.zip(gitHandlingService.listBranches(jsonTransformationDTO), Mono.just(baseArtifact));
return Mono.zip(
gitHandlingService.listReferences(jsonTransformationDTO, false), Mono.just(baseArtifact));
})
.flatMap(tuple -> {
List<String> localBranches = tuple.getT1();
Artifact baseArtifact = tuple.getT2();
GitArtifactMetadata baseGitMetadata = baseArtifact.getGitArtifactMetadata();
localBranches.remove(baseGitMetadata.getRefName());
baseArtifact.setGitArtifactMetadata(null);
gitArtifactHelper.resetAttributeInBaseArtifact(baseArtifact);
GitArtifactMetadata gitArtifactMetadata = baseArtifact.getGitArtifactMetadata();
ArtifactJsonTransformationDTO jsonTransformationDTO = new ArtifactJsonTransformationDTO();
jsonTransformationDTO.setRefType(RefType.branch);
jsonTransformationDTO.setWorkspaceId(baseArtifact.getWorkspaceId());
jsonTransformationDTO.setBaseArtifactId(gitArtifactMetadata.getDefaultArtifactId());
jsonTransformationDTO.setRepoName(gitArtifactMetadata.getRepoName());
jsonTransformationDTO.setBaseArtifactId(baseGitMetadata.getDefaultArtifactId());
jsonTransformationDTO.setRepoName(baseGitMetadata.getRepoName());
jsonTransformationDTO.setArtifactType(baseArtifact.getArtifactType());
jsonTransformationDTO.setRefName(gitArtifactMetadata.getRefName());
jsonTransformationDTO.setRefName(baseGitMetadata.getRefName());
// Remove the parent artifact branch name from the list
Mono<Boolean> removeRepoMono = gitHandlingService.removeRepository(jsonTransformationDTO);
@ -1523,7 +1534,6 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
GitType gitType) {
ArtifactType artifactType = baseArtifact.getArtifactType();
GitArtifactHelper<?> gitArtifactHelper = gitArtifactHelperResolver.getArtifactHelper(artifactType);
GitHandlingService gitHandlingService = gitHandlingServiceResolver.getGitHandlingService(gitType);
GitArtifactMetadata baseGitMetadata = baseArtifact.getGitArtifactMetadata();
@ -1738,14 +1748,12 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
jsonTransformationDTO.setBaseArtifactId(baseArtifactId);
jsonTransformationDTO.setArtifactType(baseArtifact.getArtifactType());
Path repoSuffix = gitArtifactHelper.getRepoSuffixPath(workspaceId, baseArtifactId, repoName);
return Mono.defer(() -> {
// Rehydrate the artifact from git system
Mono<MergeStatusDTO> mergeStatusDTOMono = gitHandlingService
.pullArtifact(jsonTransformationDTO, baseGitMetadata)
.cache();
Mono<ArtifactExchangeJson> artifactExchangeJsonMono = mergeStatusDTOMono.flatMap(status ->
gitHandlingService.reconstructArtifactJsonFromGitRepository(jsonTransformationDTO));
@ -1766,25 +1774,27 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
.getGitArtifactMetadata()
.getIsRepoPrivate()))
.flatMap(importedBranchedArtifact -> {
CommitDTO commitDTO = new CommitDTO();
commitDTO.setMessage(DEFAULT_COMMIT_MESSAGE
+ GitDefaultCommitMessage.SYNC_WITH_REMOTE_AFTER_PULL.getReason());
GitPullDTO gitPullDTO = new GitPullDTO();
gitPullDTO.setMergeStatus(status);
gitPullDTO.setArtifact(importedBranchedArtifact);
return gitArtifactHelper
.publishArtifact(importedBranchedArtifact, false)
// TODO: Verify if we need to commit after pulling? (Gonna be a product
// decision, hence got
.then(Mono.defer(() -> commitArtifact(
commitDTO,
baseArtifact,
importedBranchedArtifact,
gitType,
false))
.thenReturn(gitPullDTO));
.then(getGitUserForArtifactId(baseArtifactId))
.flatMap(gitAuthor -> {
CommitDTO commitDTO = new CommitDTO();
commitDTO.setMessage(DEFAULT_COMMIT_MESSAGE
+ GitDefaultCommitMessage.SYNC_WITH_REMOTE_AFTER_PULL.getReason());
commitDTO.setAuthor(gitAuthor);
GitPullDTO gitPullDTO = new GitPullDTO();
gitPullDTO.setMergeStatus(status);
gitPullDTO.setArtifact(importedBranchedArtifact);
return Mono.defer(() -> commitArtifact(
commitDTO,
baseArtifact,
importedBranchedArtifact,
gitType,
false))
.thenReturn(gitPullDTO);
});
});
});
}
@ -1960,20 +1970,21 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
}
private Mono<? extends Artifact> updateArtifactWithGitMetadataGivenPermission(
Artifact artifact, GitArtifactMetadata gitMetadata) {
Artifact branchedArtifact, GitArtifactMetadata branchedGitMetadata) {
if (gitMetadata == null) {
if (branchedGitMetadata == null) {
return Mono.error(
new AppsmithException(AppsmithError.INVALID_PARAMETER, "Git metadata values cannot be null"));
}
artifact.setGitArtifactMetadata(gitMetadata);
// For base artifact we expect a GitAuth to be a part of gitMetadata. We are using save method to leverage
// @Encrypted annotation used for private SSH keys
branchedGitMetadata.setLastCommittedAt(Instant.now());
branchedArtifact.setGitArtifactMetadata(branchedGitMetadata);
// For base branchedArtifact we expect a GitAuth to be a part of branchedGitMetadata.
// We are using save method to leverage @Encrypted annotation used for private SSH keys
// saveArtifact method sets the transient fields so no need to set it again from this method
return gitArtifactHelperResolver
.getArtifactHelper(artifact.getArtifactType())
.saveArtifact(artifact);
.getArtifactHelper(branchedArtifact.getArtifactType())
.saveArtifact(branchedArtifact);
}
/**
@ -2356,7 +2367,7 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
TRUE)
.flatMap(ignoredLock -> {
Mono<List<String>> listBranchesMono =
Mono.defer(() -> gitHandlingService.listReferences(jsonTransformationDTO, false));
Mono.defer(() -> gitHandlingService.listReferences(jsonTransformationDTO, true));
if (TRUE.equals(pruneBranches)) {
return gitHandlingService
@ -2578,6 +2589,17 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
});
}
protected Mono<? extends Artifact> generateArtifactForRefCreation(
Artifact branchedArtifact, String refName, RefType refType) {
ArtifactType artifactType = branchedArtifact.getArtifactType();
GitArtifactHelper<?> gitArtifactHelper = gitArtifactHelperResolver.getArtifactHelper(artifactType);
AclPermission editPermission = gitArtifactHelper.getArtifactEditPermission();
return gitArtifactHelper
.getArtifactById(branchedArtifact.getId(), editPermission)
.flatMap(sourceArtifact -> gitArtifactHelper.createNewArtifactForCheckout(sourceArtifact, refName));
}
/**
* In some scenarios:
* connect: after loading the modal, keyTypes is not available, so a network call has to be made to ssh-keypair.
@ -2703,7 +2725,7 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
baseArtifact, destinationArtifact, false, false, gitType)
.flatMap(destinationBranchStatus -> {
if (destinationBranchStatus.getIsClean()) {
Mono.just(destinationBranchStatus);
return Mono.just(destinationBranchStatus);
}
AppsmithException statusFailureException;
@ -2723,7 +2745,8 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
return Mono.error(statusFailureException);
}));
return sourceBranchStatusMono.zipWith(destinationBranchStatusMono);
return sourceBranchStatusMono.zipWhen(
sourceStatusDTO -> destinationBranchStatusMono);
})
.onErrorResume(error -> {
log.error(
@ -2802,7 +2825,8 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
commitDTO,
importedDestinationArtifact.getId(),
artifactType,
gitType)
gitType,
false)
.then(gitAnalyticsUtils.addAnalyticsForGitOperation(
AnalyticsEvents.GIT_MERGE,
importedDestinationArtifact,
@ -2965,7 +2989,8 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
.then(Mono.error(uncleanStatusException));
}));
return sourceBranchStatusMono.zipWith(destinationBranchStatusMono);
return sourceBranchStatusMono.zipWhen(
sourceStatusDTO -> destinationBranchStatusMono);
})
.onErrorResume(error -> {
log.error(
@ -2980,32 +3005,30 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE {
AppsmithError.GIT_ACTION_FAILED, "status", error));
});
return statusTupleMono.flatMap(statusTuple -> {
GitMergeDTO mergeDTO = new GitMergeDTO();
mergeDTO.setSourceBranch(sourceBranch);
mergeDTO.setDestinationBranch(destinationBranch);
return statusTupleMono
.flatMap(statusTuple -> {
GitMergeDTO mergeDTO = new GitMergeDTO();
mergeDTO.setSourceBranch(sourceBranch);
mergeDTO.setDestinationBranch(destinationBranch);
return gitHandlingService.isBranchMergable(jsonTransformationDTO, mergeDTO);
})
.onErrorResume(error -> {
MergeStatusDTO mergeStatus = new MergeStatusDTO();
mergeStatus.setMergeAble(false);
mergeStatus.setStatus("Merge check failed!");
mergeStatus.setMessage(error.getMessage());
Mono<MergeStatusDTO> isBranchMergable =
gitHandlingService.isBranchMergable(jsonTransformationDTO, mergeDTO);
return isBranchMergable.onErrorResume(error -> {
MergeStatusDTO mergeStatus = new MergeStatusDTO();
mergeStatus.setMergeAble(false);
mergeStatus.setStatus("Merge check failed!");
mergeStatus.setMessage(error.getMessage());
return gitAnalyticsUtils
.addAnalyticsForGitOperation(
AnalyticsEvents.GIT_MERGE_CHECK,
baseArtifact,
error.getClass().getName(),
error.getMessage(),
baseGitMetadata.getIsRepoPrivate(),
false,
false)
.thenReturn(mergeStatus);
});
});
return gitAnalyticsUtils
.addAnalyticsForGitOperation(
AnalyticsEvents.GIT_MERGE_CHECK,
baseArtifact,
error.getClass().getName(),
error.getMessage(),
baseGitMetadata.getIsRepoPrivate(),
false,
false)
.thenReturn(mergeStatus);
});
},
ignoreLock -> gitRedisUtils.releaseFileLock(artifactType, baseArtifactId, TRUE));
});

View File

@ -87,7 +87,10 @@ public interface GitHandlingServiceCE {
Mono<GitStatusDTO> getStatus(ArtifactJsonTransformationDTO jsonTransformationDTO);
Mono<String> createGitReference(ArtifactJsonTransformationDTO artifactJsonTransformationDTO, GitRefDTO gitRefDTO);
Mono<String> createGitReference(
ArtifactJsonTransformationDTO artifactJsonTransformationDTO,
GitArtifactMetadata baseGitData,
GitRefDTO gitRefDTO);
Mono<String> checkoutRemoteReference(ArtifactJsonTransformationDTO jsonTransformationDTO);

View File

@ -101,8 +101,9 @@ import static com.appsmith.external.git.constants.GitConstants.DEFAULT_COMMIT_ME
import static com.appsmith.external.git.constants.GitConstants.EMPTY_COMMIT_ERROR_MESSAGE;
import static com.appsmith.external.git.constants.GitConstants.GIT_CONFIG_ERROR;
import static com.appsmith.external.git.constants.GitConstants.GIT_PROFILE_ERROR;
import static com.appsmith.external.git.constants.ce.GitSpanCE.OPS_COMMIT;
import static com.appsmith.external.git.constants.ce.GitSpanCE.OPS_STATUS;
import static com.appsmith.external.git.constants.GitConstants.README_FILE_NAME;
import static com.appsmith.external.git.constants.GitSpan.OPS_COMMIT;
import static com.appsmith.external.git.constants.GitSpan.OPS_STATUS;
import static com.appsmith.external.helpers.AppsmithBeanUtils.copyNestedNonNullProperties;
import static com.appsmith.server.constants.ArtifactType.APPLICATION;
import static com.appsmith.server.constants.SerialiseArtifactObjective.VERSION_CONTROL;
@ -761,7 +762,7 @@ public class CommonGitServiceCEImpl implements CommonGitServiceCE {
.flatMap(artifact -> {
String repoName = GitUtils.getRepoName(gitConnectDTO.getRemoteUrl());
Path readMePath = gitArtifactHelper.getRepoSuffixPath(
artifact.getWorkspaceId(), baseArtifactId, repoName, "README.md");
artifact.getWorkspaceId(), baseArtifactId, repoName, README_FILE_NAME);
try {
Mono<Path> readMeMono = gitArtifactHelper.intialiseReadMe(artifact, readMePath, originHeader);
return Mono.zip(readMeMono, currentUserMono)

View File

@ -34,4 +34,12 @@ public class ArtifactJsonTransformationDTO {
this.baseArtifactId = baseArtifactId;
this.repoName = repoName;
}
public ArtifactJsonTransformationDTO(
String workspaceId, String baseArtifactId, String repoName, ArtifactType artifactType) {
this.workspaceId = workspaceId;
this.baseArtifactId = baseArtifactId;
this.repoName = repoName;
this.artifactType = artifactType;
}
}

View File

@ -321,7 +321,8 @@ public class GitFSServiceCEImpl implements GitHandlingServiceCE {
Path readmePath = gitArtifactHelper.getRepoSuffixPath(
jsonTransformationDTO.getWorkspaceId(),
jsonTransformationDTO.getBaseArtifactId(),
jsonTransformationDTO.getRepoName());
jsonTransformationDTO.getRepoName(),
readmeFileName);
try {
return gitArtifactHelper
.intialiseReadMe(artifact, readmePath, originHeader)
@ -688,16 +689,25 @@ public class GitFSServiceCEImpl implements GitHandlingServiceCE {
}
@Override
public Mono<String> createGitReference(ArtifactJsonTransformationDTO jsonTransformationDTO, GitRefDTO gitRefDTO) {
public Mono<String> createGitReference(
ArtifactJsonTransformationDTO jsonTransformationDTO, GitArtifactMetadata baseGitData, GitRefDTO gitRefDTO) {
GitArtifactHelper<?> gitArtifactHelper =
gitArtifactHelperResolver.getArtifactHelper(jsonTransformationDTO.getArtifactType());
String remoteUrl = baseGitData.getRemoteUrl();
String publicKey = baseGitData.getGitAuth().getPublicKey();
String privateKey = baseGitData.getGitAuth().getPrivateKey();
Path repoSuffix = gitArtifactHelper.getRepoSuffixPath(
jsonTransformationDTO.getWorkspaceId(),
jsonTransformationDTO.getBaseArtifactId(),
jsonTransformationDTO.getRepoName());
return fsGitHandler.createAndCheckoutReference(repoSuffix, gitRefDTO);
// TODO: add the checkout to the current branch as well.
return fsGitHandler
.createAndCheckoutReference(repoSuffix, gitRefDTO)
.then(fsGitHandler.pushApplication(
repoSuffix, remoteUrl, publicKey, privateKey, gitRefDTO.getRefName()));
}
@Override

View File

@ -630,8 +630,9 @@ public class CommonGitFileUtilsCE {
*/
protected void copyMetadataToArtifactExchangeJson(
GitResourceMap gitResourceMap, ArtifactExchangeJson artifactExchangeJson) {
final String metadataFilePath = CommonConstants.METADATA + JSON_EXTENSION;
GitResourceIdentity metadataIdentity =
new GitResourceIdentity(GitResourceType.ROOT_CONFIG, METADATA + JSON_EXTENSION, "");
new GitResourceIdentity(GitResourceType.ROOT_CONFIG, metadataFilePath, metadataFilePath);
Object metadata = gitResourceMap.getGitResourceMap().get(metadataIdentity);
Gson gson = new Gson();

View File

@ -0,0 +1,618 @@
package com.appsmith.server.git;
import com.appsmith.external.dtos.GitBranchDTO;
import com.appsmith.external.dtos.GitRefDTO;
import com.appsmith.external.dtos.GitStatusDTO;
import com.appsmith.external.dtos.MergeStatusDTO;
import com.appsmith.external.git.constants.ce.RefType;
import com.appsmith.git.configurations.GitServiceConfig;
import com.appsmith.git.dto.CommitDTO;
import com.appsmith.server.applications.base.ApplicationService;
import com.appsmith.server.configurations.ProjectProperties;
import com.appsmith.server.constants.ArtifactType;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.constants.GitDefaultCommitMessage;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Artifact;
import com.appsmith.server.domains.GitArtifactMetadata;
import com.appsmith.server.domains.GitProfile;
import com.appsmith.server.dtos.AutoCommitResponseDTO;
import com.appsmith.server.dtos.GitConnectDTO;
import com.appsmith.server.dtos.GitMergeDTO;
import com.appsmith.server.dtos.GitPullDTO;
import com.appsmith.server.git.autocommit.AutoCommitService;
import com.appsmith.server.git.central.CentralGitService;
import com.appsmith.server.git.central.GitType;
import com.appsmith.server.git.resolver.GitArtifactHelperResolver;
import com.appsmith.server.git.templates.contexts.GitContext;
import com.appsmith.server.git.templates.providers.GitBranchesTestTemplateProvider;
import com.appsmith.server.services.ConsolidatedAPIService;
import com.appsmith.server.services.GitArtifactHelper;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithUserDetails;
import org.testcontainers.junit.jupiter.Testcontainers;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.List;
import static com.appsmith.external.git.constants.GitConstants.DEFAULT_COMMIT_MESSAGE;
import static com.appsmith.external.git.constants.GitConstants.EMPTY_COMMIT_ERROR_MESSAGE;
import static com.appsmith.server.exceptions.AppsmithError.GIT_MERGE_FAILED_LOCAL_CHANGES;
import static com.appsmith.server.git.autocommit.AutoCommitEventHandlerImpl.AUTO_COMMIT_MSG_FORMAT;
import static org.assertj.core.api.Assertions.assertThat;
/**
* This integration test suite validates the end-to-end Git workflow for artifacts, performing a sequence of
* operations that test repository setup, branch management, status validation, and cleanup. The operations
* proceed as follows:
*
* 1. **Connect Artifact to Git**:
* - The artifact is connected to an empty Git repository using a remote URL provided by the Git server initializer.
* - A system-generated commit is created as part of the connection process.
* - Auto-commit is enabled by default, as verified in the artifact metadata.
* - The repository is checked to confirm a single system-generated commit and a clean working directory.
*
* 2. **Verify Initial Repository State**:
* - The default branch is initialized, and its name is verified to match the metadata.
* - The repository status is confirmed to be clean with no uncommitted changes.
*
* 3. **Trigger and Validate Auto-Commit**:
* - Auto-commit is triggered, and the resulting commit is validated in the Git log.
* - Commit history is checked to confirm the auto-commit appears as a second commit following the initial system-generated commit.
*
* 4. **Perform Status, Pull, and Commit Operations on the Default Branch (`master`)**:
* - The repository status is checked to confirm no changes (`isClean = true`).
* - A `pull` operation is executed to ensure synchronization, even when no updates are available.
* - A `commit` is attempted with no changes, and the response is validated to confirm no new commits were created.
*
* 5. **Create and Verify Branches**:
* - A new branch `foo` is created from the default branch (`master`).
* - Metadata for `foo` is validated, and the commit history confirms that `foo` starts from the latest commit on `master`.
* - A second branch `bar` is created from `foo`. Its metadata is verified, and the commit log confirms it starts from the latest commit on `foo`.
*
* 6. **Test Merging Scenarios**:
* - A merge from `bar` to `foo` is validated and shows no action required (`ALREADY_UP_TO_DATE`), as no changes exist.
* - Additional changes made to `bar` are merged back into `foo` successfully.
*
* 7. **Branch Deletion and Repopulation**:
* - The branch `foo` is deleted locally but repopulated from the remote repository.
* - The latest commit on `foo` is verified to match the changes made on `foo` before deletion.
* - An attempt to delete the currently checked-out branch (`master`) fails as expected.
*
* 8. **Make Changes and Validate Commits**:
* - Changes are made to the artifact on `foo` to trigger diffs.
* - The repository status is validated as `isClean = false` with pending changes.
* - A commit is created with a custom message, and the Git log confirms the commit as the latest on `foo`.
* - Changes are successfully discarded, restoring the repository to a clean state.
*
* 9. **Set and Test Branch Protection**:
* - The `master` branch is marked as protected. Commits directly to `master` are restricted.
* - Attempts to commit to `master` fail with the appropriate error message.
*
* 10. **Merge Branches (`baz` to `bar`)**:
* - A new branch `baz` is created from `bar`, and its commit log is verified.
* - Changes are made to `baz` and successfully merged into `bar` via a fast-forward merge.
* - The commit history confirms the merge, and the top commit matches the changes made in `baz`.
*
* 11. **Disconnect Artifact and Cleanup**:
* - The artifact is disconnected from the Git repository.
* - All repository branches (`foo`, `bar`, `baz`) except `master` are removed.
* - The file system is verified to confirm all repository data is cleaned up.
* - Applications associated with the deleted branches are also removed.
*
* This test suite ensures comprehensive coverage of Git workflows, including repository connection, branch creation,
* branch protection, merging, status validation, and repository cleanup. Each operation includes detailed assertions
* to validate expected outcomes and handle edge cases.
*/
@Testcontainers
@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class GitBranchesITWithCentralService {
@Autowired
@RegisterExtension
GitBranchesTestTemplateProvider templateProvider;
@Autowired
@RegisterExtension
ArtifactBuilderExtension artifactBuilderExtension;
@Autowired
@RegisterExtension
GitServerInitializerExtension gitServerInitializerExtension;
@Autowired
CentralGitService centralGitService;
@Autowired
GitTestUtils gitTestUtils;
@Autowired
GitArtifactHelperResolver gitArtifactHelperResolver;
@Autowired
GitServiceConfig gitServiceConfig;
@Autowired
AutoCommitService autoCommitService;
@Autowired
ProjectProperties projectProperties;
@Autowired
ApplicationService applicationService;
@Autowired
ConsolidatedAPIService consolidatedAPIService;
final String ORIGIN = "https://foo.bar.com";
@TestTemplate
@WithUserDetails(value = "api_user")
void test(GitContext gitContext, ExtensionContext extensionContext) throws IOException, GitAPIException, InterruptedException {
ExtensionContext.Store contextStore = extensionContext.getStore(ExtensionContext.Namespace.create(ArtifactBuilderExtension.class));
String artifactId = contextStore.get(FieldName.ARTIFACT_ID, String.class);
GitConnectDTO connectDTO = new GitConnectDTO();
connectDTO.setRemoteUrl(gitServerInitializerExtension.getGitSshUrl("test" + artifactId));
GitProfile gitProfile = new GitProfile("foo bar", "foo@bar.com", null);
connectDTO.setGitProfile(gitProfile);
// TODO:
// - Move the filePath variable to be relative, so that template name and repo name is prettier
// - Is it possible to use controller layer here? Might help with also including web filters in IT
Artifact artifact = centralGitService.connectArtifactToGit(artifactId, gitContext.getArtifactType(), connectDTO, ORIGIN, GitType.FILE_SYSTEM)
.block();
assertThat(artifact).isNotNull();
ArtifactType artifactType = artifact.getArtifactType();
GitArtifactMetadata artifactMetadata = artifact.getGitArtifactMetadata();
GitArtifactHelper<?> artifactHelper = gitArtifactHelperResolver.getArtifactHelper(artifactType);
Path repoSuffix = artifactHelper.getRepoSuffixPath(
artifact.getWorkspaceId(),
artifactMetadata.getDefaultArtifactId(),
artifactMetadata.getRepoName());
// Auto-commit should be turned on by default
assertThat(artifactMetadata.getAutoCommitConfig().getEnabled()).isTrue();
Path path = Path.of(gitServiceConfig.getGitRootPath()).resolve(repoSuffix);
String branch;
ObjectId topOfCommits;
try (Git git = Git.open(path.toFile())) {
branch = git.log().getRepository().getBranch();
assertThat(branch).isEqualTo(artifactMetadata.getRefName());
// Assert only single system generated commit exists on FS
Iterable<RevCommit> commits = git.log().call();
Iterator<RevCommit> commitIterator = commits.iterator();
assertThat(commitIterator.hasNext()).isTrue();
RevCommit firstCommit = commitIterator.next();
assertThat(firstCommit.getFullMessage()).isEqualTo(DEFAULT_COMMIT_MESSAGE + GitDefaultCommitMessage.CONNECT_FLOW.getReason());
topOfCommits = firstCommit.getId();
// TODO: Check why there is ane extra commit
assertThat(commitIterator.hasNext()).isTrue();
// Assert that git directory is clean
Status status = git.status().call();
assertThat(status.isClean()).isTrue();
}
// Assert that the artifact does have auto-commit requirements, and auto-commit gets initiated
AutoCommitResponseDTO autoCommitResponseDTO = autoCommitService.autoCommitApplication(artifactId).block();
assertThat(autoCommitResponseDTO).isNotNull();
AutoCommitResponseDTO.AutoCommitResponse autoCommitProgress = autoCommitResponseDTO.getAutoCommitResponse();
// This check requires RTS to be running on your local since client side changes come in from there
// Please make sure to run RTS before triggering this test
assertThat(autoCommitProgress).isEqualTo(AutoCommitResponseDTO.AutoCommitResponse.PUBLISHED);
// Wait for auto-commit to complete
// This should not take more than 2 seconds, we're checking every 500 ms
long startTime = System.currentTimeMillis(), currentTime = System.currentTimeMillis();
while (!autoCommitProgress.equals(AutoCommitResponseDTO.AutoCommitResponse.IDLE)) {
Thread.sleep(500);
//TODO: Undo this
if (currentTime - startTime > 2000) {
// fail("Auto-commit took too long");
}
autoCommitProgress = getAutocommitProgress(artifactId, artifact, artifactMetadata);
currentTime = System.currentTimeMillis();
}
// Now there should be two commits in the git log response
try (Git git = Git.open(path.toFile())) {
branch = git.log().getRepository().getBranch();
assertThat(branch).isEqualTo(artifactMetadata.getRefName());
Iterable<RevCommit> commits = git.log().call();
Iterator<RevCommit> commitIterator = commits.iterator();
assertThat(commitIterator.hasNext()).isTrue();
RevCommit autoCommit = commitIterator.next();
/*
org.opentest4j.AssertionFailedError:
expected: "System generated commit, to support new features in Appsmith UNKNOWN"
but was: "System generated commit, initial commit"
Expected :"System generated commit, to support new features in Appsmith UNKNOWN"
Actual :"System generated commit, initial commit"
<Click to see difference>
*/
assertThat(autoCommit.getFullMessage()).isEqualTo(String.format(AUTO_COMMIT_MSG_FORMAT, projectProperties.getVersion()));
assertThat(commitIterator.hasNext()).isTrue();
RevCommit firstCommit = commitIterator.next();
assertThat(firstCommit.getId()).isEqualTo(topOfCommits);
topOfCommits = autoCommit.getId();
}
// Assert that the initialized branch is set as default
assertThat(artifactMetadata.getRefName()).isEqualTo(artifactMetadata.getDefaultBranchName());
// Assert that the branch is not protected by default
assertThat(artifactMetadata.getBranchProtectionRules()).isNullOrEmpty();
// Check that the status is clean
GitStatusDTO statusDTO = centralGitService.getStatus(artifactId, artifactType, true, GitType.FILE_SYSTEM).block();
assertThat(statusDTO).isNotNull();
assertThat(statusDTO.getIsClean()).isTrue();
assertThat(statusDTO.getAheadCount()).isEqualTo(0);
assertThat(statusDTO.getBehindCount()).isEqualTo(0);
// Check that pull when not required, still goes through
GitPullDTO gitPullDTO = centralGitService.pullArtifact(artifactId, artifactType, GitType.FILE_SYSTEM).block();
assertThat(gitPullDTO).isNotNull();
// Check that commit says that there is nothing to commit
CommitDTO newCommitDTO = new CommitDTO();
newCommitDTO.setMessage("Unused message");
String commitResponse = centralGitService.commitArtifact(newCommitDTO, artifactId, artifactType, GitType.FILE_SYSTEM)
.block();
assertThat(commitResponse).contains(EMPTY_COMMIT_ERROR_MESSAGE);
// Check that the previous attempt didn't actually go through
try (Git git = Git.open(path.toFile())) {
branch = git.log().getRepository().getBranch();
assertThat(branch).isEqualTo(artifactMetadata.getRefName());
Iterable<RevCommit> commits = git.log().call();
assertThat(commits.iterator().next().getId()).isEqualTo(topOfCommits);
}
// Check that discard, even when not required, goes through
Artifact discardedArtifact = centralGitService.discardChanges(artifactId, artifactType, GitType.FILE_SYSTEM).block();
assertThat(discardedArtifact).isNotNull();
// Make a change in the artifact to trigger a diff
gitTestUtils.createADiffInArtifact(artifact).block();
// Check that the status is not clean
GitStatusDTO statusDTO2 = centralGitService.getStatus(artifactId, artifactType, true, GitType.FILE_SYSTEM).block();
assertThat(statusDTO2).isNotNull();
assertThat(statusDTO2.getIsClean()).isFalse();
assertThat(statusDTO2.getAheadCount()).isEqualTo(0);
assertThat(statusDTO2.getBehindCount()).isEqualTo(0);
// Check that commit makes the custom message be the top of the log
CommitDTO commitDTO2 = new CommitDTO();
commitDTO2.setMessage("Custom message");
String commitResponse2 = centralGitService.commitArtifact(commitDTO2, artifactId, artifactType, GitType.FILE_SYSTEM)
.block();
assertThat(commitResponse2).contains("Committed successfully!");
try (Git git = Git.open(path.toFile())) {
branch = git.log().getRepository().getBranch();
assertThat(branch).isEqualTo(artifactMetadata.getRefName());
Iterable<RevCommit> commits = git.log().call();
Iterator<RevCommit> commitIterator = commits.iterator();
RevCommit newCommit = commitIterator.next();
assertThat(newCommit.getFullMessage()).isEqualTo("Custom message");
assertThat(commitIterator.next().getId()).isEqualTo(topOfCommits);
topOfCommits = newCommit.getId();
}
// Check that status is clean again
GitStatusDTO statusDTO3 = centralGitService.getStatus(artifactId, artifactType, true, GitType.FILE_SYSTEM).block();
assertThat(statusDTO3).isNotNull();
assertThat(statusDTO3.getIsClean()).isTrue();
assertThat(statusDTO3.getAheadCount()).isEqualTo(0);
assertThat(statusDTO3.getBehindCount()).isEqualTo(0);
// Make another change to trigger a diff
gitTestUtils.createADiffInArtifact(artifact).block();
// Check that status in not clean
GitStatusDTO statusDTO4 = centralGitService.getStatus(artifactId, artifactType, true, GitType.FILE_SYSTEM ).block();
assertThat(statusDTO4).isNotNull();
assertThat(statusDTO4.getIsClean()).isFalse();
assertThat(statusDTO4.getAheadCount()).isEqualTo(0);
assertThat(statusDTO4.getBehindCount()).isEqualTo(0);
// Protect the master branch
List<String> protectedBranches = centralGitService.updateProtectedBranches(artifactId, artifactType, List.of(branch)).block();
assertThat(protectedBranches).containsExactly(branch);
// Now try to commit, and check that it fails
CommitDTO commitDTO3 = new CommitDTO();
commitDTO3.setMessage("Failed commit");
Mono<String> commitResponse3Mono = centralGitService.commitArtifact(commitDTO3, artifactId, artifactType, GitType.FILE_SYSTEM);
StepVerifier.create(commitResponse3Mono)
.expectErrorSatisfies(e -> assertThat(e.getMessage()).contains("Cannot commit to protected branch"))
.verify();
// Create a new branch foo from master, check that the commit for new branch is created as system generated
// On top of the previous custom commit
GitRefDTO gitRefDTO = new GitRefDTO();
gitRefDTO.setRefName("foo");
gitRefDTO.setRefType(RefType.branch);
Artifact fooArtifact = centralGitService.createReference(artifactId, artifactType, gitRefDTO, GitType.FILE_SYSTEM).block();
assertThat(fooArtifact).isNotNull();
String fooArtifactId = fooArtifact.getId();
GitArtifactMetadata fooMetadata = fooArtifact.getGitArtifactMetadata();
assertThat(fooMetadata.getRefName()).isEqualTo("foo");
try (Git git = Git.open(path.toFile())) {
// since the new flow discards the parent branch,
// the parent branch is checkedOut at last.
Iterable<RevCommit> commits = git.log().call();
Iterator<RevCommit> commitIterator = commits.iterator();
RevCommit newCommit = commitIterator.next();
assertThat(newCommit.getId()).isEqualTo(topOfCommits);
}
// Since the status
// Check that status on foo, it should be dirty
GitStatusDTO statusDTO5 = centralGitService.getStatus(fooArtifactId, artifactType, true, GitType.FILE_SYSTEM).block();
assertThat(statusDTO5).isNotNull();
assertThat(statusDTO5.getIsClean()).isFalse();
assertThat(statusDTO5.getAheadCount()).isEqualTo(0);
assertThat(statusDTO5.getBehindCount()).isEqualTo(0);
// Create another branch bar from foo
GitRefDTO barBranchDTO = new GitRefDTO();
barBranchDTO.setRefName("bar");
barBranchDTO.setRefType(RefType.branch);
Artifact barArtifact = centralGitService.createReference(fooArtifactId, artifactType, barBranchDTO, GitType.FILE_SYSTEM).block();
assertThat(barArtifact).isNotNull();
String barArtifactId = barArtifact.getId();
GitArtifactMetadata barMetadata = barArtifact.getGitArtifactMetadata();
assertThat(barMetadata.getRefName()).isEqualTo("bar");
try (Git git = Git.open(path.toFile())) {
Iterable<RevCommit> commits = git.log().call();
Iterator<RevCommit> commitIterator = commits.iterator();
assertThat(commitIterator.next().getId()).isEqualTo(topOfCommits);
}
// Check that status on foo,
// it should be clean as the diff is transferred to new branch bar
GitStatusDTO fooStatusDTO = centralGitService.getStatus(fooArtifactId, artifactType, true, GitType.FILE_SYSTEM).block();
assertThat(fooStatusDTO).isNotNull();
assertThat(fooStatusDTO.getIsClean()).isTrue();
assertThat(fooStatusDTO.getAheadCount()).isEqualTo(0);
assertThat(fooStatusDTO.getBehindCount()).isEqualTo(0);
// Check that status on bar, it should be dirty
GitStatusDTO barStatusDTO = centralGitService.getStatus(barArtifactId, artifactType, true, GitType.FILE_SYSTEM).block();
assertThat(barStatusDTO).isNotNull();
assertThat(barStatusDTO.getIsClean()).isFalse();
assertThat(barStatusDTO.getAheadCount()).isEqualTo(0);
assertThat(barStatusDTO.getBehindCount()).isEqualTo(0);
Artifact discardBarBranch = centralGitService.discardChanges(barArtifactId, artifactType, GitType.FILE_SYSTEM).block();
assertThat(discardBarBranch).isNotNull();
// Check merge status to foo shows no action required
// bar -> foo
GitMergeDTO gitMergeDTO = new GitMergeDTO();
gitMergeDTO.setDestinationBranch("foo");
gitMergeDTO.setSourceBranch("bar");
MergeStatusDTO mergeStatusDTO = centralGitService.isBranchMergable(barArtifactId, artifactType, gitMergeDTO, GitType.FILE_SYSTEM ).block();
assertThat(mergeStatusDTO).isNotNull();
assertThat(mergeStatusDTO.getStatus()).isEqualTo("ALREADY_UP_TO_DATE");
// Delete foo locally and re-populate from remote
List<String> branchList = centralGitService.listBranchForArtifact(artifactId, artifactType, false, GitType.FILE_SYSTEM)
.flatMapMany(Flux::fromIterable)
.map(GitBranchDTO::getBranchName)
.collectList()
.block();
assertThat(branchList).containsExactlyInAnyOrder(
artifactMetadata.getRefName(),
"origin/" + artifactMetadata.getRefName(),
fooMetadata.getRefName(),
"origin/" + fooMetadata.getRefName(),
barMetadata.getRefName(),
"origin/" + barMetadata.getRefName());
Mono<? extends Artifact> deleteBranchAttemptMono = centralGitService.deleteGitReference(artifactId, artifactType, gitRefDTO, GitType.FILE_SYSTEM);
StepVerifier
.create(deleteBranchAttemptMono)
.expectErrorSatisfies(e -> assertThat(e.getMessage()).contains("Cannot delete current checked out branch"))
.verify();
// TODO: I'm having to checkout myself to be able to delete the branch.
// Are we relying on auto-commit check to do this otherwise?
// Is this a potential bug?
try (Git git = Git.open(path.toFile())) {
git.checkout().setName("bar").call();
}
GitRefDTO deleteFooDTO = new GitRefDTO();
deleteFooDTO.setRefType(RefType.branch);
deleteFooDTO.setRefName("foo");
centralGitService.deleteGitReference(artifactId, artifactType, deleteFooDTO, GitType.FILE_SYSTEM).block();
List<String> branchList2 = centralGitService.listBranchForArtifact(artifactId, artifactType, false, GitType.FILE_SYSTEM)
.flatMapMany(Flux::fromIterable)
.map(GitBranchDTO::getBranchName)
.collectList()
.block();
assertThat(branchList2).containsExactlyInAnyOrder(
artifactMetadata.getRefName(),
"origin/" + artifactMetadata.getRefName(),
"origin/" + fooMetadata.getRefName(),
barMetadata.getRefName(),
"origin/" + barMetadata.getRefName());
GitRefDTO checkoutFooDTO = new GitRefDTO();
checkoutFooDTO.setRefName("origin/foo");
checkoutFooDTO.setRefType(RefType.branch);
Artifact checkedOutFooArtifact = centralGitService.checkoutReference(artifactId, artifactType, checkoutFooDTO, true, GitType.FILE_SYSTEM).block();
assertThat(checkedOutFooArtifact).isNotNull();
List<String> branchList3 = centralGitService.listBranchForArtifact(artifactId, artifactType, false, GitType.FILE_SYSTEM)
.flatMapMany(Flux::fromIterable)
.map(GitBranchDTO::getBranchName)
.collectList()
.block();
assertThat(branchList3).containsExactlyInAnyOrder(
artifactMetadata.getRefName(),
"origin/" + artifactMetadata.getRefName(),
fooMetadata.getRefName(),
"origin/" + fooMetadata.getRefName(),
barMetadata.getRefName(),
"origin/" + barMetadata.getRefName());
// Verify latest commit on foo should be same as changes made on foo previously
try (Git git = Git.open(path.toFile())) {
branch = git.log().getRepository().getBranch();
assertThat(branch).isEqualTo(fooMetadata.getRefName());
Iterable<RevCommit> commits = git.log().call();
Iterator<RevCommit> commitIterator = commits.iterator();
assertThat(commitIterator.next().getId()).isEqualTo(topOfCommits);
}
// Make more changes on foo and attempt discard
gitTestUtils.createADiffInArtifact(checkedOutFooArtifact).block();
GitStatusDTO discardableStatus = centralGitService.getStatus(checkedOutFooArtifact.getId(), artifactType, false, GitType.FILE_SYSTEM).block();
assertThat(discardableStatus).isNotNull();
assertThat(discardableStatus.getIsClean()).isFalse();
Artifact discardedFoo = centralGitService.discardChanges(checkedOutFooArtifact.getId(), artifactType, GitType.FILE_SYSTEM).block();
GitStatusDTO discardedStatus = centralGitService.getStatus(checkedOutFooArtifact.getId(), artifactType, false, GitType.FILE_SYSTEM).block();
assertThat(discardedStatus).isNotNull();
// TODO: Why is this not clean?
// There is an on page load that gets triggered here that is causing a diff
// This should ideally have already been fixed on initial artifact import
// assertThat(discardedStatus.getIsClean()).isTrue();
// Make a change to trigger a diff on bar
gitTestUtils.createADiffInArtifact(barArtifact).block();
// Check merge status to master shows not merge-able
GitMergeDTO gitMergeDTO2 = new GitMergeDTO();
gitMergeDTO2.setSourceBranch("bar");
gitMergeDTO2.setDestinationBranch("master");
MergeStatusDTO mergeStatusDTO2 = centralGitService.isBranchMergable(barArtifactId, artifactType, gitMergeDTO2, GitType.FILE_SYSTEM).block();
assertThat(mergeStatusDTO2).isNotNull();
assertThat(mergeStatusDTO2.isMergeAble()).isFalse();
assertThat(mergeStatusDTO2.getMessage()).contains(GIT_MERGE_FAILED_LOCAL_CHANGES.getMessage("bar"));
// Create a new branch baz and check for new commit
GitRefDTO gitBranchDTO = new GitRefDTO();
gitBranchDTO.setRefName("baz");
gitBranchDTO.setRefType(RefType.branch);
Artifact bazArtifact = centralGitService.createReference(barArtifactId, artifactType, gitBranchDTO, GitType.FILE_SYSTEM).block();
assertThat(bazArtifact).isNotNull();
try (Git git = Git.open(path.toFile())) {
Iterable<RevCommit> commits = git.log().call();
Iterator<RevCommit> commitIterator = commits.iterator();
RevCommit newCommit = commitIterator.next();
assertThat(newCommit.getId()).isEqualTo(topOfCommits);
}
GitStatusDTO bazStatus = centralGitService.getStatus(bazArtifact.getId(), artifactType, true, GitType.FILE_SYSTEM).block();
assertThat(bazStatus).isNotNull();
assertThat(bazStatus.getIsClean()).isFalse();
centralGitService.discardChanges(bazArtifact.getId(), artifactType, GitType.FILE_SYSTEM).block();
GitStatusDTO bazStatus2 = centralGitService.getStatus(bazArtifact.getId(), artifactType, true, GitType.FILE_SYSTEM).block();
assertThat(bazStatus2).isNotNull();
assertThat(bazStatus2.getIsClean()).isTrue();
GitMergeDTO gitMergeDTO3 = new GitMergeDTO();
gitMergeDTO3.setSourceBranch("baz");
gitMergeDTO3.setDestinationBranch("bar");
MergeStatusDTO mergeStatusDTO3 = centralGitService.isBranchMergable(bazArtifact.getId(), artifactType, gitMergeDTO3, GitType.FILE_SYSTEM).block();
assertThat(mergeStatusDTO3).isNotNull();
assertThat(mergeStatusDTO3.isMergeAble()).isTrue();
GitStatusDTO barStatus2 = centralGitService.getStatus(barArtifactId, artifactType, true, GitType.FILE_SYSTEM).block();
assertThat(barStatus2).isNotNull();
assertThat(barStatus2.getIsClean()).isTrue();
MergeStatusDTO barToBazMergeStatus = centralGitService.mergeBranch(bazArtifact.getId(), artifactType, gitMergeDTO3, GitType.FILE_SYSTEM).block();
assertThat(barToBazMergeStatus).isNotNull();
assertThat(barToBazMergeStatus.isMergeAble()).isTrue();
assertThat(barToBazMergeStatus.getStatus()).contains("ALREADY_UP_TO_DATE");
// Since fast-forward should succeed here, top of commit should not change
try (Git git = Git.open(path.toFile())) {
Iterable<RevCommit> commits = git.log().call();
Iterator<RevCommit> commitIterator = commits.iterator();
assertThat(commitIterator.next().getId()).isEqualTo(topOfCommits);
}
// Disconnect artifact and verify non-existence of `foo`, `bar` and `baz`
Artifact disconnectedArtifact = centralGitService.detachRemote(artifactId, artifactType, GitType.FILE_SYSTEM).block();
assertThat(disconnectedArtifact).isNotNull();
assertThat(disconnectedArtifact.getGitArtifactMetadata()).isNull();
// TODO: This needs to be generified for artifacts
Application deletedFooArtifact = applicationService.findById(checkedOutFooArtifact.getId()).block();
assertThat(deletedFooArtifact).isNull();
Application deletedBarArtifact = applicationService.findById(barArtifactId).block();
assertThat(deletedBarArtifact).isNull();
Application deletedBazArtifact = applicationService.findById(bazArtifact.getId()).block();
assertThat(deletedBazArtifact).isNull();
Application existingMasterArtifact = applicationService.findById(artifactId).block();
assertThat(existingMasterArtifact).isNotNull();
// Verify FS is clean after disconnect
boolean repoDirectoryNotExists = Files.notExists(path);
assertThat(repoDirectoryNotExists).isTrue();
}
private AutoCommitResponseDTO.AutoCommitResponse getAutocommitProgress(String artifactId, Artifact artifact, GitArtifactMetadata artifactMetadata) {
AutoCommitResponseDTO autoCommitProgress = centralGitService.getAutoCommitProgress(artifactId, artifact.getArtifactType(), artifactMetadata.getRefName()).block();
assertThat(autoCommitProgress).isNotNull();
return autoCommitProgress.getAutoCommitResponse();
}
}