chore: prelim fixes for the in mem git (#41201)
## 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/17826532521> > Commit: d7e0d5646396a25ffc73c9444a57200993868926 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=17826532521&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.Git` > Spec: > <hr>Thu, 18 Sep 2025 11:50: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 - Branch-aware Git clone/checkout with Redis-backed caching and automatic cleanup. - Operation-aware Git routing for endpoints. - Enhanced, timestamped logging for Git scripts. - Improvements - Faster, more reliable Git flows with lock-based FSM orchestration. - Consistent merge behavior that honors “keep working directory changes.” - Improved private key handling for SSH. - Error Handling - Clearer, granular Git error messages for metadata, FS ops, Redis download, and cleanup. - Documentation - Updated Git route flow documentation. - Tests - Extensive unit tests covering routing, metadata checks, cleanup gating, and key flows. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
20da6c6aef
commit
f7293cfa05
|
|
@ -63,9 +63,7 @@ public class BashService {
|
||||||
"Bash execution failed: " + buildErrorDetails(output, error, exceptionError, exitCode));
|
"Bash execution failed: " + buildErrorDetails(output, error, exceptionError, exitCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Script: {}", fullScript);
|
log.info("Output: \n{}", output);
|
||||||
log.info("Output: {}", output);
|
|
||||||
log.info("Error: {}", error);
|
|
||||||
log.info("Exit code: {}", exitCode);
|
log.info("Exit code: {}", exitCode);
|
||||||
|
|
||||||
outputStream.close();
|
outputStream.close();
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,23 @@
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Simple logging helpers
|
||||||
|
log_ts() {
|
||||||
|
date '+%Y-%m-%dT%H:%M:%S%z'
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info() {
|
||||||
|
printf '[%s] [INFO] %s\n' "$(log_ts)" "$*" >&1
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warn() {
|
||||||
|
printf '[%s] [WARN] %s\n' "$(log_ts)" "$*" >&1
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
printf '[%s] [ERROR] %s\n' "$(log_ts)" "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
# Time-to-live for git artifacts in Redis (24 hours in seconds)
|
# Time-to-live for git artifacts in Redis (24 hours in seconds)
|
||||||
GIT_ARTIFACT_TTL=86400
|
GIT_ARTIFACT_TTL=86400
|
||||||
|
|
||||||
|
|
@ -25,7 +42,22 @@ git_clone() {
|
||||||
|
|
||||||
git -C "$target_folder" init "$target_folder" --initial-branch=none
|
git -C "$target_folder" init "$target_folder" --initial-branch=none
|
||||||
git -C "$target_folder" remote add origin "$remote_url"
|
git -C "$target_folder" remote add origin "$remote_url"
|
||||||
GIT_SSH_COMMAND="ssh -i $temp_private_key -o StrictHostKeyChecking=no" git -C "$target_folder" fetch origin --depth=1
|
GIT_SSH_COMMAND="ssh -i $temp_private_key -o StrictHostKeyChecking=no" git -C "$target_folder" fetch origin
|
||||||
|
}
|
||||||
|
|
||||||
|
git_clean_up() {
|
||||||
|
local redis_key="$1"
|
||||||
|
local redis_url="$2"
|
||||||
|
local target_folder="$3"
|
||||||
|
local key_value_pair_key="$4"
|
||||||
|
|
||||||
|
trap 'rm -rf "'"$target_folder"'"' EXIT ERR
|
||||||
|
|
||||||
|
## delete the repository from redis
|
||||||
|
redis-cli -u "$redis_url" DEL "$redis_key"
|
||||||
|
|
||||||
|
## delete the repository branch_store
|
||||||
|
redis-cli -u "$redis_url" DEL "$key_value_pair_key"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Uploads git repo to Redis as compressed archive
|
# Uploads git repo to Redis as compressed archive
|
||||||
|
|
@ -33,14 +65,30 @@ git_upload() {
|
||||||
local redis_key="$1"
|
local redis_key="$1"
|
||||||
local redis_url="$2"
|
local redis_url="$2"
|
||||||
local target_folder="$3"
|
local target_folder="$3"
|
||||||
|
local key_value_pair_key="$4"
|
||||||
|
|
||||||
trap 'rm -rf "'"$target_folder"'"' EXIT ERR
|
trap 'rm -rf "'"$target_folder"'"' EXIT ERR
|
||||||
|
|
||||||
rm -f "$target_folder/.git/index.lock"
|
rm -f "$target_folder/.git/index.lock"
|
||||||
|
|
||||||
|
upload_branches_to_redis_hash "$target_folder" "$redis_url" "$key_value_pair_key"
|
||||||
tar -cf - -C "$target_folder" . | zstd -q --threads=0 | base64 -w 0 | redis-cli -u "$redis_url" --raw -x SETEX "$redis_key" "$GIT_ARTIFACT_TTL"
|
tar -cf - -C "$target_folder" . | zstd -q --threads=0 | base64 -w 0 | redis-cli -u "$redis_url" --raw -x SETEX "$redis_key" "$GIT_ARTIFACT_TTL"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
upload_branches_to_redis_hash() {
|
||||||
|
# get all local branches and their latest commit sha
|
||||||
|
local directory_path="$1"
|
||||||
|
local redis_url="$2"
|
||||||
|
local key_value_pair_key="$3"
|
||||||
|
|
||||||
|
local branches=$(git -C "$directory_path" for-each-ref --format='"%(refname:short)" %(objectname:short)' refs/heads/)
|
||||||
|
|
||||||
|
log_info "Preparing to upload branch store. Current branches: $branches"
|
||||||
|
|
||||||
|
redis-cli -u "$redis_url" DEL "$key_value_pair_key"
|
||||||
|
redis-cli -u "$redis_url" HSET "$key_value_pair_key" $branches
|
||||||
|
}
|
||||||
|
|
||||||
# Downloads git repo from Redis or clones if not cached
|
# Downloads git repo from Redis or clones if not cached
|
||||||
git_download() {
|
git_download() {
|
||||||
local author_email="$1"
|
local author_email="$1"
|
||||||
|
|
@ -50,6 +98,9 @@ git_download() {
|
||||||
local redis_url="$5"
|
local redis_url="$5"
|
||||||
local remote_url="$6"
|
local remote_url="$6"
|
||||||
local target_folder="$7"
|
local target_folder="$7"
|
||||||
|
local key_value_pair_key="$8"
|
||||||
|
|
||||||
|
log_info "Searching for repository: $target_folder with key: $redis_key in redis."
|
||||||
|
|
||||||
rm -rf "$target_folder"
|
rm -rf "$target_folder"
|
||||||
mkdir -p "$target_folder"
|
mkdir -p "$target_folder"
|
||||||
|
|
@ -57,24 +108,141 @@ git_download() {
|
||||||
if [ "$(redis-cli -u "$redis_url" --raw EXISTS "$redis_key")" = "1" ]; then
|
if [ "$(redis-cli -u "$redis_url" --raw EXISTS "$redis_key")" = "1" ]; then
|
||||||
redis-cli -u "$redis_url" --raw GET "$redis_key" | base64 -d | zstd -d --threads=0 | tar -xf - -C "$target_folder"
|
redis-cli -u "$redis_url" --raw GET "$redis_key" | base64 -d | zstd -d --threads=0 | tar -xf - -C "$target_folder"
|
||||||
else
|
else
|
||||||
git_clone "$private_key" "$remote_url" "$target_folder"
|
log_warn "Cache miss. Repository: $target_folder with key: $redis_key does not exist in redis."
|
||||||
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -f "$target_folder/.git/index.lock"
|
rm -f "$target_folder/.git/index.lock"
|
||||||
|
git -C "$target_folder" reset --hard
|
||||||
git -C "$target_folder" config user.name "$author_name"
|
git -C "$target_folder" config user.name "$author_name"
|
||||||
git -C "$target_folder" config user.email "$author_email"
|
git -C "$target_folder" config user.email "$author_email"
|
||||||
git -C "$target_folder" config fetch.parallel 4
|
}
|
||||||
|
|
||||||
git -C "$target_folder" reset --hard
|
git_clone_and_checkout() {
|
||||||
|
local author_email="$1"
|
||||||
|
local author_name="$2"
|
||||||
|
local private_key="$3"
|
||||||
|
local remote_url="$4"
|
||||||
|
local target_folder="$5"
|
||||||
|
local redis_url="$6"
|
||||||
|
local key_value_pair_key="$7"
|
||||||
|
|
||||||
# Checkout all branches
|
## branches are after argument 7
|
||||||
for remote in $(git -C "$target_folder" branch -r | grep -vE 'origin/HEAD'); do
|
|
||||||
branch=${remote#origin/}
|
trap 'rm -rf "'"$target_folder"'"' ERR
|
||||||
if ! git -C "$target_folder" show-ref --quiet "refs/heads/$branch"; then
|
|
||||||
git -C "$target_folder" checkout -b "$branch" "$remote" || true
|
log_info "target_folder: $target_folder, remote_url: $remote_url"
|
||||||
|
## remove the repository directory entirely
|
||||||
|
rm -rf "$target_folder"
|
||||||
|
|
||||||
|
## create the same directory
|
||||||
|
mkdir -p "$target_folder"
|
||||||
|
|
||||||
|
git_clone "$private_key" "$remote_url" "$target_folder"
|
||||||
|
git -C "$target_folder" config user.name "$author_name"
|
||||||
|
git -C "$target_folder" config user.email "$author_email"
|
||||||
|
git -C "$target_folder" config fetch.parallel 4
|
||||||
|
git -C "$target_folder" reflog expire --expire=now --all
|
||||||
|
git -C "$target_folder" gc --prune=now --aggressive
|
||||||
|
|
||||||
|
# This provides all the arguments from arg 5 onwards to the function git_co_from_redis.
|
||||||
|
# This includes the target folder, redis url, key value pair key, and all branch names from db.
|
||||||
|
git_checkout_from_branch_store ${@:5}
|
||||||
|
}
|
||||||
|
|
||||||
|
git_checkout_from_branch_store() {
|
||||||
|
local repository_path="$1"
|
||||||
|
local redis_url="$2"
|
||||||
|
local key_value_pair_key="$3"
|
||||||
|
|
||||||
|
# 4th onwards args are branches names from db
|
||||||
|
# fetch raw value
|
||||||
|
log_info "Searching Redis branch-store for key : $key_value_pair_key"
|
||||||
|
local raw
|
||||||
|
raw=$(redis-cli -u "$redis_url" --raw HGETALL "$key_value_pair_key" | sed 's/\"//g')
|
||||||
|
|
||||||
|
# error handling: empty or missing key
|
||||||
|
if [[ -z "$raw" ]]; then
|
||||||
|
log_warn "No value found for key '$key_value_pair_key'. Initiating checkouts from database branch names"
|
||||||
|
git_checkout_all_branches "$repository_path" "${@:4}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read into an array line by line (handles special chars, no word splitting)
|
||||||
|
arr=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
arr+=( "$line" )
|
||||||
|
done <<< "$raw"
|
||||||
|
|
||||||
|
# Iterate through pairs: arr[0]=branch, arr[1]=sha ...
|
||||||
|
local error_return_value=0
|
||||||
|
for ((i=0; i<${#arr[@]}; i+=2)); do
|
||||||
|
local branch="${arr[i]}"
|
||||||
|
local commit="${arr[i+1]}"
|
||||||
|
|
||||||
|
# Skip incomplete pair just in case
|
||||||
|
if [[ -z "$branch" || -z "$commit" ]]; then
|
||||||
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# call the fallback function
|
||||||
|
git_checkout_at_commit "$repository_path" "$branch" "$commit" || {
|
||||||
|
log_warn "Git_checkout_at_commit failed for $branch $commit"
|
||||||
|
error_return_value=1
|
||||||
|
}
|
||||||
|
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if [[ $error_return_value -eq 1 ]]; then
|
||||||
|
log_warn "git_checkout_at_commit failed for some branches. Initiating checkouts from database branch names"
|
||||||
|
git_checkout_all_branches "$repository_path" "${@:4}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to checkout all local branches in a repository with args from db branch names
|
||||||
|
git_checkout_all_branches() {
|
||||||
|
local target_folder="$1"
|
||||||
|
|
||||||
|
# From arg 2 onwards, all args are local branch names populated from db.
|
||||||
|
# Checkout all local branches
|
||||||
|
for arg in "${@:2}"; do
|
||||||
|
local local_branch=$arg
|
||||||
|
log_info "Checking out branch $local_branch from database"
|
||||||
|
git -C "$target_folder" checkout $local_branch || log_error "Git checkout failed for $local_branch"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to checkout a branch at a specific commit in a specified directory
|
||||||
|
# If branch exists, it will reset it to the given SHA
|
||||||
|
# If branch doesn't exist, it will create it at the given SHA
|
||||||
|
git_checkout_at_commit() {
|
||||||
|
# Check if we have exactly 3 arguments
|
||||||
|
if [ $# -ne 3 ]; then
|
||||||
|
log_info "Usage: git_checkout_at_commit <directory-path> <branch-name> <commit-sha>"
|
||||||
|
log_info "Example: git_checkout_at_commit /path/to/repo feature-branch abc123def"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local directory_path="$1"
|
||||||
|
local branch_name="$2"
|
||||||
|
local commit_sha="$3"
|
||||||
|
|
||||||
|
# Check if directory exists
|
||||||
|
if [ ! -d "$directory_path" ]; then
|
||||||
|
log_error "Directory '$directory_path' does not exist"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Checkout the branch
|
||||||
|
git -C "$directory_path" checkout $branch_name
|
||||||
|
|
||||||
|
# Hard reset to the specified commit
|
||||||
|
git -C "$directory_path" reset --hard "$commit_sha"
|
||||||
|
|
||||||
|
log_info "✓ Branch '$branch_name' has been reset to commit $commit_sha in '$directory_path'"
|
||||||
}
|
}
|
||||||
|
|
||||||
git_merge_branch() {
|
git_merge_branch() {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.appsmith.server.annotations;
|
package com.appsmith.server.annotations;
|
||||||
|
|
||||||
import com.appsmith.server.constants.ArtifactType;
|
import com.appsmith.server.constants.ArtifactType;
|
||||||
|
import com.appsmith.server.git.constants.GitRouteOperation;
|
||||||
|
|
||||||
import java.lang.annotation.Documented;
|
import java.lang.annotation.Documented;
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
|
|
@ -15,4 +16,6 @@ public @interface GitRoute {
|
||||||
String fieldName();
|
String fieldName();
|
||||||
|
|
||||||
ArtifactType artifactType();
|
ArtifactType artifactType();
|
||||||
|
|
||||||
|
GitRouteOperation operation();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
package com.appsmith.server.artifacts.gitRoute;
|
package com.appsmith.server.artifacts.gitRoute;
|
||||||
|
|
||||||
|
import com.appsmith.server.git.resolver.GitArtifactHelperResolver;
|
||||||
import com.appsmith.server.repositories.ApplicationRepository;
|
import com.appsmith.server.repositories.ApplicationRepository;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class GitRouteArtifact extends GitRouteArtifactCE {
|
public class GitRouteArtifact extends GitRouteArtifactCE {
|
||||||
|
|
||||||
public GitRouteArtifact(ApplicationRepository applicationRepository) {
|
public GitRouteArtifact(
|
||||||
super(applicationRepository);
|
ApplicationRepository applicationRepository, GitArtifactHelperResolver gitArtifactHelperResolver) {
|
||||||
|
super(applicationRepository, gitArtifactHelperResolver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,21 @@ import com.appsmith.server.constants.ArtifactType;
|
||||||
import com.appsmith.server.domains.Artifact;
|
import com.appsmith.server.domains.Artifact;
|
||||||
import com.appsmith.server.exceptions.AppsmithError;
|
import com.appsmith.server.exceptions.AppsmithError;
|
||||||
import com.appsmith.server.exceptions.AppsmithException;
|
import com.appsmith.server.exceptions.AppsmithException;
|
||||||
|
import com.appsmith.server.git.resolver.GitArtifactHelperResolver;
|
||||||
import com.appsmith.server.repositories.ApplicationRepository;
|
import com.appsmith.server.repositories.ApplicationRepository;
|
||||||
|
import com.appsmith.server.services.GitArtifactHelper;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public abstract class GitRouteArtifactCE {
|
public abstract class GitRouteArtifactCE {
|
||||||
|
protected final GitArtifactHelperResolver gitArtifactHelperResolver;
|
||||||
protected final ApplicationRepository applicationRepository;
|
protected final ApplicationRepository applicationRepository;
|
||||||
|
|
||||||
public GitRouteArtifactCE(ApplicationRepository applicationRepository) {
|
public GitRouteArtifactCE(
|
||||||
|
ApplicationRepository applicationRepository, GitArtifactHelperResolver gitArtifactHelperResolver) {
|
||||||
this.applicationRepository = applicationRepository;
|
this.applicationRepository = applicationRepository;
|
||||||
|
this.gitArtifactHelperResolver = gitArtifactHelperResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Mono<Artifact> getArtifact(ArtifactType artifactType, String artifactId) {
|
public Mono<Artifact> getArtifact(ArtifactType artifactType, String artifactId) {
|
||||||
|
|
@ -25,4 +30,8 @@ public abstract class GitRouteArtifactCE {
|
||||||
default -> Mono.error(new AppsmithException(AppsmithError.GIT_ROUTE_HANDLER_NOT_FOUND, artifactType));
|
default -> Mono.error(new AppsmithException(AppsmithError.GIT_ROUTE_HANDLER_NOT_FOUND, artifactType));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GitArtifactHelper<? extends Artifact> getArtifactHelper(ArtifactType artifactType) {
|
||||||
|
return this.gitArtifactHelperResolver.getArtifactHelper(artifactType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package com.appsmith.server.aspect;
|
package com.appsmith.server.aspect;
|
||||||
|
|
||||||
|
import com.appsmith.external.git.constants.GitSpan;
|
||||||
|
import com.appsmith.external.git.constants.ce.RefType;
|
||||||
import com.appsmith.git.configurations.GitServiceConfig;
|
import com.appsmith.git.configurations.GitServiceConfig;
|
||||||
import com.appsmith.git.service.BashService;
|
import com.appsmith.git.service.BashService;
|
||||||
import com.appsmith.server.annotations.GitRoute;
|
import com.appsmith.server.annotations.GitRoute;
|
||||||
|
|
@ -10,8 +12,11 @@ import com.appsmith.server.domains.GitArtifactMetadata;
|
||||||
import com.appsmith.server.domains.GitAuth;
|
import com.appsmith.server.domains.GitAuth;
|
||||||
import com.appsmith.server.domains.GitProfile;
|
import com.appsmith.server.domains.GitProfile;
|
||||||
import com.appsmith.server.exceptions.AppsmithError;
|
import com.appsmith.server.exceptions.AppsmithError;
|
||||||
|
import com.appsmith.server.exceptions.AppsmithErrorCode;
|
||||||
import com.appsmith.server.exceptions.AppsmithException;
|
import com.appsmith.server.exceptions.AppsmithException;
|
||||||
import com.appsmith.server.git.utils.GitProfileUtils;
|
import com.appsmith.server.git.utils.GitProfileUtils;
|
||||||
|
import com.appsmith.server.services.GitArtifactHelper;
|
||||||
|
import io.micrometer.observation.ObservationRegistry;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
@ -28,19 +33,29 @@ import org.bouncycastle.util.io.pem.PemReader;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import reactor.core.observability.micrometer.Micrometer;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.util.retry.Retry;
|
||||||
|
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Path;
|
||||||
import java.security.KeyFactory;
|
import java.security.KeyFactory;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
import static com.appsmith.server.helpers.GitUtils.MAX_RETRIES;
|
||||||
|
import static com.appsmith.server.helpers.GitUtils.RETRY_DELAY;
|
||||||
|
import static org.springframework.util.StringUtils.hasText;
|
||||||
|
|
||||||
@Aspect
|
@Aspect
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
|
@ -48,26 +63,37 @@ import java.util.stream.IntStream;
|
||||||
public class GitRouteAspect {
|
public class GitRouteAspect {
|
||||||
|
|
||||||
private static final Duration LOCK_TTL = Duration.ofSeconds(90);
|
private static final Duration LOCK_TTL = Duration.ofSeconds(90);
|
||||||
|
private static final String REDIS_FILE_LOCK_VALUE = "inUse";
|
||||||
|
|
||||||
|
private static final String RUN_ERROR_MESSAGE_FORMAT = "An error occurred during state: %s of git. Error: %s";
|
||||||
private static final String REDIS_REPO_KEY_FORMAT = "purpose=repo/v=1/workspace=%s/artifact=%s/repository=%s/";
|
private static final String REDIS_REPO_KEY_FORMAT = "purpose=repo/v=1/workspace=%s/artifact=%s/repository=%s/";
|
||||||
|
private static final String REDIS_LOCK_KEY_FORMAT = "purpose=lock/%s";
|
||||||
|
private static final String REDIS_BRANCH_STORE_FORMAT = "branchStore=%s";
|
||||||
|
private static final String BASH_COMMAND_FILE = "git.sh";
|
||||||
|
private static final String GIT_UPLOAD = "git_upload";
|
||||||
|
private static final String GIT_DOWNLOAD = "git_download";
|
||||||
|
private static final String GIT_CLONE = "git_clone_and_checkout";
|
||||||
|
private static final String GIT_CLEAN_UP = "git_clean_up";
|
||||||
|
|
||||||
private final ReactiveRedisTemplate<String, String> redis;
|
private final ReactiveRedisTemplate<String, String> redis;
|
||||||
private final GitProfileUtils gitProfileUtils;
|
private final GitProfileUtils gitProfileUtils;
|
||||||
private final GitServiceConfig gitServiceConfig;
|
private final GitServiceConfig gitServiceConfig;
|
||||||
private final GitRouteArtifact gitRouteArtifact;
|
private final GitRouteArtifact gitRouteArtifact;
|
||||||
private final BashService bashService = new BashService();
|
private final BashService bashService = new BashService();
|
||||||
|
private final ObservationRegistry observationRegistry;
|
||||||
|
|
||||||
@Value("${appsmith.redis.git.url}")
|
@Value("${appsmith.redis.git.url}")
|
||||||
private String redisUrl;
|
private String redisUrl;
|
||||||
|
|
||||||
@Value("${appsmith.git.root}")
|
|
||||||
private String gitRootPath;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* FSM: Definitions
|
* FSM: Definitions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private enum State {
|
private enum State {
|
||||||
ARTIFACT,
|
ARTIFACT,
|
||||||
|
ROUTE_FILTER,
|
||||||
|
METADATA_FILTER,
|
||||||
|
UNROUTED_EXECUTION,
|
||||||
PARENT,
|
PARENT,
|
||||||
GIT_META,
|
GIT_META,
|
||||||
REPO_KEY,
|
REPO_KEY,
|
||||||
|
|
@ -78,7 +104,11 @@ public class GitRouteAspect {
|
||||||
GIT_KEY,
|
GIT_KEY,
|
||||||
REPO_PATH,
|
REPO_PATH,
|
||||||
DOWNLOAD,
|
DOWNLOAD,
|
||||||
|
FETCH_BRANCHES,
|
||||||
|
CLONE,
|
||||||
EXECUTE,
|
EXECUTE,
|
||||||
|
CLEAN_UP_FILTER,
|
||||||
|
CLEAN_UP,
|
||||||
UPLOAD,
|
UPLOAD,
|
||||||
UNLOCK,
|
UNLOCK,
|
||||||
RESULT,
|
RESULT,
|
||||||
|
|
@ -115,6 +145,9 @@ public class GitRouteAspect {
|
||||||
|
|
||||||
// Tasks
|
// Tasks
|
||||||
private Artifact artifact;
|
private Artifact artifact;
|
||||||
|
private Boolean routeFilter;
|
||||||
|
private Boolean metadataFilter;
|
||||||
|
private Boolean cleanUpFilter;
|
||||||
private Artifact parent;
|
private Artifact parent;
|
||||||
private GitArtifactMetadata gitMeta;
|
private GitArtifactMetadata gitMeta;
|
||||||
private String repoKey;
|
private String repoKey;
|
||||||
|
|
@ -124,19 +157,31 @@ public class GitRouteAspect {
|
||||||
private GitAuth gitAuth;
|
private GitAuth gitAuth;
|
||||||
private String gitKey;
|
private String gitKey;
|
||||||
private String repoPath;
|
private String repoPath;
|
||||||
|
private String branchStoreKey;
|
||||||
private Object download;
|
private Object download;
|
||||||
private Object execute;
|
private Object execute;
|
||||||
private Object upload;
|
private Object upload;
|
||||||
|
private Object cleanUp;
|
||||||
private Boolean unlock;
|
private Boolean unlock;
|
||||||
private Object result;
|
private Object result;
|
||||||
|
private List<String> localBranches;
|
||||||
|
private Object clone;
|
||||||
|
|
||||||
// Errors
|
// Errors
|
||||||
private Throwable error;
|
private AppsmithException error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refer to GitRouteAspect.md#gitroute-fsm-execution-flow for the FSM diagram.
|
// Refer to GitRouteAspect.md#gitroute-fsm-execution-flow for the FSM diagram.
|
||||||
private final Map<State, StateConfig> FSM = Map.ofEntries(
|
private final Map<State, StateConfig> FSM = Map.ofEntries(
|
||||||
Map.entry(State.ARTIFACT, new StateConfig(State.PARENT, State.RESULT, "artifact", this::artifact)),
|
Map.entry(
|
||||||
|
State.ROUTE_FILTER,
|
||||||
|
new StateConfig(State.ARTIFACT, State.UNROUTED_EXECUTION, "routeFilter", this::routeFilter)),
|
||||||
|
Map.entry(State.UNROUTED_EXECUTION, new StateConfig(State.RESULT, State.RESULT, "execute", this::execute)),
|
||||||
|
Map.entry(State.RESULT, new StateConfig(State.DONE, State.DONE, "result", this::result)),
|
||||||
|
Map.entry(State.ARTIFACT, new StateConfig(State.METADATA_FILTER, State.RESULT, "artifact", this::artifact)),
|
||||||
|
Map.entry(
|
||||||
|
State.METADATA_FILTER,
|
||||||
|
new StateConfig(State.PARENT, State.UNROUTED_EXECUTION, "metadataFilter", this::metadataFilter)),
|
||||||
Map.entry(State.PARENT, new StateConfig(State.GIT_META, State.RESULT, "parent", this::parent)),
|
Map.entry(State.PARENT, new StateConfig(State.GIT_META, State.RESULT, "parent", this::parent)),
|
||||||
Map.entry(State.GIT_META, new StateConfig(State.REPO_KEY, State.RESULT, "gitMeta", this::gitMeta)),
|
Map.entry(State.GIT_META, new StateConfig(State.REPO_KEY, State.RESULT, "gitMeta", this::gitMeta)),
|
||||||
Map.entry(State.REPO_KEY, new StateConfig(State.LOCK_KEY, State.RESULT, "repoKey", this::repoKey)),
|
Map.entry(State.REPO_KEY, new StateConfig(State.LOCK_KEY, State.RESULT, "repoKey", this::repoKey)),
|
||||||
|
|
@ -146,17 +191,35 @@ public class GitRouteAspect {
|
||||||
Map.entry(State.GIT_AUTH, new StateConfig(State.GIT_KEY, State.UNLOCK, "gitAuth", this::gitAuth)),
|
Map.entry(State.GIT_AUTH, new StateConfig(State.GIT_KEY, State.UNLOCK, "gitAuth", this::gitAuth)),
|
||||||
Map.entry(State.GIT_KEY, new StateConfig(State.REPO_PATH, State.UNLOCK, "gitKey", this::gitKey)),
|
Map.entry(State.GIT_KEY, new StateConfig(State.REPO_PATH, State.UNLOCK, "gitKey", this::gitKey)),
|
||||||
Map.entry(State.REPO_PATH, new StateConfig(State.DOWNLOAD, State.UNLOCK, "repoPath", this::repoPath)),
|
Map.entry(State.REPO_PATH, new StateConfig(State.DOWNLOAD, State.UNLOCK, "repoPath", this::repoPath)),
|
||||||
Map.entry(State.DOWNLOAD, new StateConfig(State.EXECUTE, State.UNLOCK, "download", this::download)),
|
Map.entry(
|
||||||
Map.entry(State.EXECUTE, new StateConfig(State.UPLOAD, State.UPLOAD, "execute", this::execute)),
|
State.DOWNLOAD,
|
||||||
|
new StateConfig(State.EXECUTE, State.FETCH_BRANCHES, "download", this::downloadFromRedis)),
|
||||||
|
Map.entry(
|
||||||
|
State.EXECUTE,
|
||||||
|
new StateConfig(State.CLEAN_UP_FILTER, State.CLEAN_UP_FILTER, "execute", this::execute)),
|
||||||
|
Map.entry(
|
||||||
|
State.CLEAN_UP_FILTER,
|
||||||
|
new StateConfig(State.UPLOAD, State.CLEAN_UP, "cleanUpFilter", this::cleanUpFilter)),
|
||||||
Map.entry(State.UPLOAD, new StateConfig(State.UNLOCK, State.UNLOCK, "upload", this::upload)),
|
Map.entry(State.UPLOAD, new StateConfig(State.UNLOCK, State.UNLOCK, "upload", this::upload)),
|
||||||
|
Map.entry(State.CLEAN_UP, new StateConfig(State.UNLOCK, State.UNLOCK, "cleanUp", this::cleanUp)),
|
||||||
Map.entry(State.UNLOCK, new StateConfig(State.RESULT, State.RESULT, "unlock", this::unlock)),
|
Map.entry(State.UNLOCK, new StateConfig(State.RESULT, State.RESULT, "unlock", this::unlock)),
|
||||||
Map.entry(State.RESULT, new StateConfig(State.DONE, State.DONE, "result", this::result)));
|
Map.entry(
|
||||||
|
State.FETCH_BRANCHES,
|
||||||
|
new StateConfig(State.CLONE, State.UNLOCK, "localBranches", this::getLocalBranches)),
|
||||||
|
Map.entry(State.CLONE, new StateConfig(State.EXECUTE, State.UNLOCK, "clone", this::clone)));
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* FSM: Runners
|
* FSM: Runners
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Entry point for Git operations
|
/**
|
||||||
|
* Entry point advice for methods annotated with {@link GitRoute}. When Git is configured to operate in-memory,
|
||||||
|
* this initializes the FSM context and executes the Git-aware flow; otherwise, proceeds directly.
|
||||||
|
*
|
||||||
|
* @param joinPoint the intercepted join point
|
||||||
|
* @param gitRoute the {@link GitRoute} annotation from the intercepted method
|
||||||
|
* @return the result of the intercepted method, possibly wrapped via FSM flow
|
||||||
|
*/
|
||||||
@Around("@annotation(gitRoute)")
|
@Around("@annotation(gitRoute)")
|
||||||
public Object handleGitRoute(ProceedingJoinPoint joinPoint, GitRoute gitRoute) {
|
public Object handleGitRoute(ProceedingJoinPoint joinPoint, GitRoute gitRoute) {
|
||||||
Context ctx = new Context().setJoinPoint(joinPoint).setGitRoute(gitRoute);
|
Context ctx = new Context().setJoinPoint(joinPoint).setGitRoute(gitRoute);
|
||||||
|
|
@ -167,14 +230,20 @@ public class GitRouteAspect {
|
||||||
}
|
}
|
||||||
|
|
||||||
String fieldValue = extractFieldValue(joinPoint, gitRoute.fieldName());
|
String fieldValue = extractFieldValue(joinPoint, gitRoute.fieldName());
|
||||||
|
|
||||||
ctx.setFieldValue(fieldValue);
|
ctx.setFieldValue(fieldValue);
|
||||||
|
return run(ctx, State.ROUTE_FILTER).flatMap(unused -> {
|
||||||
return run(ctx, State.ARTIFACT)
|
return this.result(ctx);
|
||||||
.flatMap(unused -> ctx.getError() != null ? Mono.error(ctx.getError()) : Mono.just(ctx.getResult()));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// State machine executor
|
/**
|
||||||
|
* Executes the Git FSM by evaluating the function associated with the current state and transitioning based
|
||||||
|
* on the outcome until reaching the DONE state.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM execution context
|
||||||
|
* @param current the current {@link State}
|
||||||
|
* @return a Mono that completes when the FSM reaches the DONE state
|
||||||
|
*/
|
||||||
private Mono<Object> run(Context ctx, State current) {
|
private Mono<Object> run(Context ctx, State current) {
|
||||||
if (current == State.DONE) {
|
if (current == State.DONE) {
|
||||||
return Mono.just(true);
|
return Mono.just(true);
|
||||||
|
|
@ -188,52 +257,182 @@ public class GitRouteAspect {
|
||||||
.flatMap(result -> {
|
.flatMap(result -> {
|
||||||
setContextField(ctx, config.getContextField(), result);
|
setContextField(ctx, config.getContextField(), result);
|
||||||
long duration = System.currentTimeMillis() - startTime;
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
log.info("State: {}, SUCCESS: {}, Time: {}ms", current, result, duration);
|
|
||||||
|
// selective logging of information
|
||||||
|
if (State.REPO_KEY.equals(current) || State.LOCK_KEY.equals(current)) {
|
||||||
|
log.info(
|
||||||
|
"Operation : {}, State {} : {}, Data : {}, Time : {}ms",
|
||||||
|
ctx.getGitRoute().operation(),
|
||||||
|
current,
|
||||||
|
Outcome.SUCCESS.name(),
|
||||||
|
result,
|
||||||
|
duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"Operation : {}, State {} : {}, Time: {}ms",
|
||||||
|
ctx.getGitRoute().operation(),
|
||||||
|
current,
|
||||||
|
Outcome.SUCCESS.name(),
|
||||||
|
duration);
|
||||||
return run(ctx, config.next(Outcome.SUCCESS));
|
return run(ctx, config.next(Outcome.SUCCESS));
|
||||||
})
|
})
|
||||||
.onErrorResume(e -> {
|
.onErrorResume(e -> {
|
||||||
ctx.setError(e);
|
if (e instanceof AppsmithException appsmithException) {
|
||||||
|
ctx.setError(appsmithException);
|
||||||
|
} else {
|
||||||
|
ctx.setError(new AppsmithException(
|
||||||
|
AppsmithError.GIT_ACTION_FAILED,
|
||||||
|
ctx.getGitRoute().operation().toString().toLowerCase(),
|
||||||
|
String.format(RUN_ERROR_MESSAGE_FORMAT, current.name(), e.getMessage())));
|
||||||
|
}
|
||||||
|
|
||||||
long duration = System.currentTimeMillis() - startTime;
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
log.info("State: {}, FAIL: {}, Time: {}ms", current, e.getMessage(), duration);
|
log.error(
|
||||||
|
"Operation : {}, State {} : {}, Error : {}, Time: {}ms",
|
||||||
|
ctx.getGitRoute().operation(),
|
||||||
|
current,
|
||||||
|
Outcome.FAIL.name(),
|
||||||
|
e.getMessage(),
|
||||||
|
duration);
|
||||||
return run(ctx, config.next(Outcome.FAIL));
|
return run(ctx, config.next(Outcome.FAIL));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* FSM: Tasks
|
* Attempts to acquire a Redis-based lock for the given key, storing the git command as value.
|
||||||
|
*
|
||||||
|
* @param key the redis lock key
|
||||||
|
* @param gitCommand the git command being executed (used for diagnostics)
|
||||||
|
* @return Mono emitting true if lock acquired, or error if already locked
|
||||||
*/
|
*/
|
||||||
|
private Mono<Boolean> setLock(String key, String gitCommand) {
|
||||||
|
String command = hasText(gitCommand) ? gitCommand : REDIS_FILE_LOCK_VALUE;
|
||||||
|
|
||||||
// Acquires Redis lock
|
return redis.opsForValue().setIfAbsent(key, command, LOCK_TTL).flatMap(locked -> {
|
||||||
private Mono<Boolean> lock(Context ctx) {
|
if (Boolean.TRUE.equals(locked)) {
|
||||||
return redis.opsForValue()
|
return Mono.just(Boolean.TRUE);
|
||||||
.setIfAbsent(ctx.getLockKey(), "1", LOCK_TTL)
|
}
|
||||||
.flatMap(locked -> locked
|
|
||||||
? Mono.just(true)
|
return redis.opsForValue()
|
||||||
: Mono.error(new AppsmithException(AppsmithError.GIT_FILE_IN_USE, ctx.getLockKey())));
|
.get(key)
|
||||||
|
.flatMap(commandName ->
|
||||||
|
Mono.error(new AppsmithException(AppsmithError.GIT_FILE_IN_USE, command, commandName)));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finds artifact
|
/**
|
||||||
|
* Acquires a Redis lock with retry semantics for transient contention scenarios.
|
||||||
|
*
|
||||||
|
* @param key the redis lock key
|
||||||
|
* @param gitCommand the git command associated with the lock
|
||||||
|
* @return Mono emitting true on successful lock acquisition, or error after retries exhaust
|
||||||
|
*/
|
||||||
|
private Mono<Boolean> addLockWithRetry(String key, String gitCommand) {
|
||||||
|
return this.setLock(key, gitCommand)
|
||||||
|
.retryWhen(Retry.fixedDelay(MAX_RETRIES, RETRY_DELAY)
|
||||||
|
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> {
|
||||||
|
if (retrySignal.failure() instanceof AppsmithException) {
|
||||||
|
throw (AppsmithException) retrySignal.failure();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AppsmithException(AppsmithError.GIT_FILE_IN_USE, gitCommand);
|
||||||
|
}))
|
||||||
|
.name(GitSpan.ADD_FILE_LOCK)
|
||||||
|
.tap(Micrometer.observation(observationRegistry));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FSM state: acquire the Redis lock for the current repository operation.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM execution context containing lock key and operation
|
||||||
|
* @return Mono emitting true when lock is acquired
|
||||||
|
*/
|
||||||
|
private Mono<Boolean> lock(Context ctx) {
|
||||||
|
String key = ctx.getLockKey();
|
||||||
|
String command = ctx.getGitRoute().operation().name();
|
||||||
|
|
||||||
|
return this.addLockWithRetry(key, command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FSM state: resolve the target {@link Artifact} for the operation based on annotation metadata.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM execution context
|
||||||
|
* @return Mono emitting the resolved Artifact
|
||||||
|
*/
|
||||||
private Mono<?> artifact(Context ctx) {
|
private Mono<?> artifact(Context ctx) {
|
||||||
ArtifactType artifactType = ctx.getGitRoute().artifactType();
|
ArtifactType artifactType = ctx.getGitRoute().artifactType();
|
||||||
String artifactId = ctx.getFieldValue();
|
String artifactId = ctx.getFieldValue();
|
||||||
return gitRouteArtifact.getArtifact(artifactType, artifactId);
|
return gitRouteArtifact.getArtifact(artifactType, artifactId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finds parent artifact
|
/**
|
||||||
|
* This method finds out if the current operation requires git operation.
|
||||||
|
* @param ctx : context
|
||||||
|
* @return: A mono which emits a boolean, else errors out.
|
||||||
|
*/
|
||||||
|
private Mono<?> routeFilter(Context ctx) {
|
||||||
|
if (ctx.getGitRoute().operation().requiresGitOperation()) {
|
||||||
|
return Mono.just(Boolean.TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mono.error(new AppsmithException(
|
||||||
|
AppsmithError.GIT_ROUTE_FS_OPS_NOT_REQUIRED, ctx.getGitRoute().operation()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FSM state: validate that the artifact has sufficient Git metadata to require filesystem operations.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM execution context
|
||||||
|
* @return Mono emitting true if metadata is present and valid, or error otherwise
|
||||||
|
*/
|
||||||
|
private Mono<?> metadataFilter(Context ctx) {
|
||||||
|
// if the metadata is null, default artifact id, remote url, or reponame is not present,
|
||||||
|
// then that means that at this point, either this artifact is not git connected.
|
||||||
|
Artifact artifact = ctx.getArtifact();
|
||||||
|
GitArtifactMetadata metadata = artifact.getGitArtifactMetadata();
|
||||||
|
|
||||||
|
if (metadata == null
|
||||||
|
|| !StringUtils.hasText(metadata.getDefaultArtifactId())
|
||||||
|
|| !StringUtils.hasText(metadata.getRemoteUrl())
|
||||||
|
|| !StringUtils.hasText(metadata.getRepoName())) {
|
||||||
|
return Mono.error(new AppsmithException(AppsmithError.GIT_ROUTE_METADATA_NOT_FOUND));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mono.just(Boolean.TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FSM state: resolve the parent artifact (by default artifact id) for repository-scoped operations.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM execution context
|
||||||
|
* @return Mono emitting the parent Artifact
|
||||||
|
*/
|
||||||
private Mono<?> parent(Context ctx) {
|
private Mono<?> parent(Context ctx) {
|
||||||
ArtifactType artifactType = ctx.getGitRoute().artifactType();
|
ArtifactType artifactType = ctx.getGitRoute().artifactType();
|
||||||
String parentArtifactId = ctx.getArtifact().getGitArtifactMetadata().getDefaultArtifactId();
|
String parentArtifactId = ctx.getArtifact().getGitArtifactMetadata().getDefaultArtifactId();
|
||||||
return gitRouteArtifact.getArtifact(artifactType, parentArtifactId);
|
return gitRouteArtifact.getArtifact(artifactType, parentArtifactId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validates Git metadata
|
/**
|
||||||
|
* FSM state: validate that Git metadata exists on the parent artifact.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM execution context
|
||||||
|
* @return Mono emitting the {@link GitArtifactMetadata} or an error if missing
|
||||||
|
*/
|
||||||
private Mono<?> gitMeta(Context ctx) {
|
private Mono<?> gitMeta(Context ctx) {
|
||||||
return Mono.justOrEmpty(ctx.getParent().getGitArtifactMetadata())
|
return Mono.justOrEmpty(ctx.getParent().getGitArtifactMetadata())
|
||||||
.switchIfEmpty(Mono.error(new AppsmithException(
|
.switchIfEmpty(Mono.error(new AppsmithException(
|
||||||
AppsmithError.INVALID_GIT_CONFIGURATION, "Git metadata is not configured")));
|
AppsmithError.INVALID_GIT_CONFIGURATION, "Git metadata is not configured")));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generates Redis repo key
|
/**
|
||||||
|
* FSM state: generate the canonical Redis repository key for the operation.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM execution context
|
||||||
|
* @return Mono emitting the repository key string
|
||||||
|
*/
|
||||||
private Mono<?> repoKey(Context ctx) {
|
private Mono<?> repoKey(Context ctx) {
|
||||||
String key = String.format(
|
String key = String.format(
|
||||||
REDIS_REPO_KEY_FORMAT,
|
REDIS_REPO_KEY_FORMAT,
|
||||||
|
|
@ -243,13 +442,23 @@ public class GitRouteAspect {
|
||||||
return Mono.just(key);
|
return Mono.just(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generates Redis lock key
|
/**
|
||||||
|
* FSM state: generate the lock key from the repository key.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM execution context
|
||||||
|
* @return Mono emitting the lock key string
|
||||||
|
*/
|
||||||
private Mono<?> lockKey(Context ctx) {
|
private Mono<?> lockKey(Context ctx) {
|
||||||
String key = String.format("purpose=lock/%s", ctx.getRepoKey());
|
String key = String.format(REDIS_LOCK_KEY_FORMAT, ctx.getRepoKey());
|
||||||
return Mono.just(key);
|
return Mono.just(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets Git user profile
|
/**
|
||||||
|
* FSM state: resolve the current user's {@link GitProfile}.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM execution context
|
||||||
|
* @return Mono emitting the Git profile, or error if not configured
|
||||||
|
*/
|
||||||
private Mono<GitProfile> gitProfile(Context ctx) {
|
private Mono<GitProfile> gitProfile(Context ctx) {
|
||||||
return gitProfileUtils
|
return gitProfileUtils
|
||||||
.getGitProfileForUser(ctx.getFieldValue())
|
.getGitProfileForUser(ctx.getFieldValue())
|
||||||
|
|
@ -257,49 +466,130 @@ public class GitRouteAspect {
|
||||||
AppsmithError.INVALID_GIT_CONFIGURATION, "Git profile is not configured")));
|
AppsmithError.INVALID_GIT_CONFIGURATION, "Git profile is not configured")));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validates Git auth
|
/**
|
||||||
|
* FSM state: validate presence of {@link GitAuth} on the artifact metadata.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM execution context
|
||||||
|
* @return Mono emitting the GitAuth or error if missing
|
||||||
|
*/
|
||||||
private Mono<?> gitAuth(Context ctx) {
|
private Mono<?> gitAuth(Context ctx) {
|
||||||
return Mono.justOrEmpty(ctx.getGitMeta().getGitAuth())
|
return Mono.justOrEmpty(ctx.getGitMeta().getGitAuth())
|
||||||
.switchIfEmpty(Mono.error(new AppsmithException(
|
.switchIfEmpty(Mono.error(new AppsmithException(
|
||||||
AppsmithError.INVALID_GIT_CONFIGURATION, "Git authentication is not configured")));
|
AppsmithError.INVALID_GIT_CONFIGURATION, "Git authentication is not configured")));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Processes Git SSH key
|
/**
|
||||||
|
* FSM state: process and normalize the SSH private key for Git operations.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM execution context
|
||||||
|
* @return Mono emitting a normalized private key string or error if processing fails
|
||||||
|
*/
|
||||||
private Mono<?> gitKey(Context ctx) {
|
private Mono<?> gitKey(Context ctx) {
|
||||||
try {
|
try {
|
||||||
return Mono.just(processPrivateKey(
|
return Mono.just(processPrivateKey(
|
||||||
ctx.getGitAuth().getPrivateKey(), ctx.getGitAuth().getPublicKey()));
|
ctx.getGitAuth().getPrivateKey(), ctx.getGitAuth().getPublicKey()));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return Mono.error(new AppsmithException(
|
return Mono.error(new AppsmithException(
|
||||||
AppsmithError.INVALID_GIT_CONFIGURATION, "Failed to process private key: " + e.getMessage()));
|
AppsmithError.GIT_ROUTE_INVALID_PRIVATE_KEY, "Failed to process private key: " + e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets local repo path
|
/**
|
||||||
|
* FSM state: compute the local repository path for the artifact and set the branch store key.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM execution context
|
||||||
|
* @return Mono emitting the absolute repository path as string
|
||||||
|
*/
|
||||||
private Mono<?> repoPath(Context ctx) {
|
private Mono<?> repoPath(Context ctx) {
|
||||||
var path = Paths.get(
|
// this needs to be changed based on artifact as well.
|
||||||
gitRootPath,
|
Path repositorySuffixPath = gitRouteArtifact
|
||||||
ctx.getArtifact().getWorkspaceId(),
|
.getArtifactHelper(ctx.getGitRoute().artifactType())
|
||||||
ctx.getGitMeta().getDefaultArtifactId(),
|
.getRepoSuffixPath(
|
||||||
ctx.getGitMeta().getRepoName());
|
ctx.getArtifact().getWorkspaceId(),
|
||||||
return Mono.just(path.toString());
|
ctx.getGitMeta().getDefaultArtifactId(),
|
||||||
|
ctx.getGitMeta().getRepoName());
|
||||||
|
|
||||||
|
ctx.setBranchStoreKey(String.format(REDIS_BRANCH_STORE_FORMAT, repositorySuffixPath));
|
||||||
|
Path repositoryPath = Path.of(gitServiceConfig.getGitRootPath()).resolve(repositorySuffixPath);
|
||||||
|
return Mono.just(repositoryPath.toAbsolutePath().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Downloads Git repo
|
/**
|
||||||
private Mono<?> download(Context ctx) {
|
* FSM state: download repository content from Redis-backed storage into the working directory.
|
||||||
return bashService.callFunction(
|
*
|
||||||
"git.sh",
|
* @param ctx the FSM execution context
|
||||||
"git_download",
|
* @return Mono signaling completion of download script execution
|
||||||
ctx.getGitProfile().getAuthorEmail(),
|
*/
|
||||||
|
private Mono<?> downloadFromRedis(Context ctx) {
|
||||||
|
return bashService
|
||||||
|
.callFunction(
|
||||||
|
BASH_COMMAND_FILE,
|
||||||
|
GIT_DOWNLOAD,
|
||||||
|
ctx.getGitProfile().getAuthorEmail(),
|
||||||
|
ctx.getGitProfile().getAuthorName(),
|
||||||
|
ctx.getGitKey(),
|
||||||
|
ctx.getRepoKey(),
|
||||||
|
redisUrl,
|
||||||
|
ctx.getGitMeta().getRemoteUrl(),
|
||||||
|
ctx.getRepoPath(),
|
||||||
|
ctx.getBranchStoreKey())
|
||||||
|
.onErrorResume(error -> Mono.error(
|
||||||
|
new AppsmithException(AppsmithError.GIT_ROUTE_REDIS_DOWNLOAD_FAILED, error.getMessage())));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FSM state: fetch all local branch ref names for the parent artifact base id.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM execution context
|
||||||
|
* @return Mono emitting a list of local branch names
|
||||||
|
*/
|
||||||
|
private Mono<?> getLocalBranches(Context ctx) {
|
||||||
|
GitArtifactHelper<?> gitArtifactHelper =
|
||||||
|
gitRouteArtifact.getArtifactHelper(ctx.getGitRoute().artifactType());
|
||||||
|
return gitArtifactHelper
|
||||||
|
.getAllArtifactByBaseId(ctx.getParent().getGitArtifactMetadata().getDefaultArtifactId(), null)
|
||||||
|
.filter(artifact -> {
|
||||||
|
if (artifact.getGitArtifactMetadata() == null
|
||||||
|
|| RefType.tag.equals(
|
||||||
|
artifact.getGitArtifactMetadata().getRefType())) {
|
||||||
|
return Boolean.FALSE;
|
||||||
|
}
|
||||||
|
return Boolean.TRUE;
|
||||||
|
})
|
||||||
|
.map(artifact -> artifact.getGitArtifactMetadata().getRefName())
|
||||||
|
.collectList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FSM state: fallback clone and checkout flow when download fails; clones remote and checks out known branches.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM execution context
|
||||||
|
* @return Mono signaling completion of clone script execution
|
||||||
|
*/
|
||||||
|
private Mono<?> clone(Context ctx) {
|
||||||
|
List<String> metaArgs = List.of(
|
||||||
|
ctx.getGitProfile().getAuthorName(),
|
||||||
ctx.getGitProfile().getAuthorName(),
|
ctx.getGitProfile().getAuthorName(),
|
||||||
ctx.getGitKey(),
|
ctx.getGitKey(),
|
||||||
ctx.getRepoKey(),
|
|
||||||
redisUrl,
|
|
||||||
ctx.getGitMeta().getRemoteUrl(),
|
ctx.getGitMeta().getRemoteUrl(),
|
||||||
ctx.getRepoPath());
|
ctx.getRepoPath(),
|
||||||
|
redisUrl,
|
||||||
|
ctx.getBranchStoreKey());
|
||||||
|
|
||||||
|
List<String> completeArgs = new ArrayList<>(metaArgs);
|
||||||
|
completeArgs.addAll(ctx.localBranches);
|
||||||
|
|
||||||
|
String[] varArgs = completeArgs.toArray(new String[0]);
|
||||||
|
|
||||||
|
return bashService.callFunction(BASH_COMMAND_FILE, GIT_CLONE, varArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Executes Git operation
|
/**
|
||||||
|
* FSM state: proceed the intercepted join point (business logic) within the Git-managed flow.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM execution context
|
||||||
|
* @return Mono emitting the intercepted method's result or error
|
||||||
|
*/
|
||||||
private Mono<?> execute(Context ctx) {
|
private Mono<?> execute(Context ctx) {
|
||||||
try {
|
try {
|
||||||
return (Mono<Object>) ctx.getJoinPoint().proceed();
|
return (Mono<Object>) ctx.getJoinPoint().proceed();
|
||||||
|
|
@ -308,26 +598,90 @@ public class GitRouteAspect {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uploads Git changes
|
/**
|
||||||
private Mono<?> upload(Context ctx) {
|
* FSM state: This method finds out if redis and FS cleanup is required
|
||||||
return bashService.callFunction("git.sh", "git_upload", ctx.getRepoKey(), redisUrl, ctx.getRepoPath());
|
*
|
||||||
|
* @param ctx the FSM execution context
|
||||||
|
* @return Mono emitting the intercepted method's result or error
|
||||||
|
*/
|
||||||
|
private Mono<?> cleanUpFilter(Context ctx) {
|
||||||
|
// if clean up is not required then proceed
|
||||||
|
if (!ctx.getGitRoute().operation().gitCleanUp()) {
|
||||||
|
return Mono.just(Boolean.TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mono.error(new AppsmithException(
|
||||||
|
AppsmithError.GIT_ROUTE_FS_CLEAN_UP_REQUIRED, ctx.getGitRoute().operation()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Releases Redis lock
|
/**
|
||||||
|
* FSM state: upload local repository changes to Redis-backed storage.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM execution context
|
||||||
|
* @return Mono signaling completion of upload script execution
|
||||||
|
*/
|
||||||
|
private Mono<?> cleanUp(Context ctx) {
|
||||||
|
return bashService.callFunction(
|
||||||
|
BASH_COMMAND_FILE,
|
||||||
|
GIT_CLEAN_UP,
|
||||||
|
ctx.getRepoKey(),
|
||||||
|
redisUrl,
|
||||||
|
ctx.getRepoPath(),
|
||||||
|
ctx.getBranchStoreKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FSM state: upload local repository changes to Redis-backed storage.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM execution context
|
||||||
|
* @return Mono signaling completion of upload script execution
|
||||||
|
*/
|
||||||
|
private Mono<?> upload(Context ctx) {
|
||||||
|
return bashService.callFunction(
|
||||||
|
BASH_COMMAND_FILE, GIT_UPLOAD, ctx.getRepoKey(), redisUrl, ctx.getRepoPath(), ctx.getBranchStoreKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FSM state: release the Redis lock acquired for the repository.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM execution context
|
||||||
|
* @return Mono emitting true if the lock was released
|
||||||
|
*/
|
||||||
private Mono<?> unlock(Context ctx) {
|
private Mono<?> unlock(Context ctx) {
|
||||||
return redis.delete(ctx.getLockKey()).map(count -> count > 0);
|
return redis.delete(ctx.getLockKey()).map(count -> count > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns operation result
|
/**
|
||||||
|
* FSM state: finalize by returning the intercepted method execution result, or propagate an error when
|
||||||
|
* appropriate based on the error state.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM execution context
|
||||||
|
* @return Mono emitting the business method result or an error
|
||||||
|
*/
|
||||||
private Mono<?> result(Context ctx) {
|
private Mono<?> result(Context ctx) {
|
||||||
return ctx.getError() != null ? Mono.error(ctx.getError()) : Mono.just(ctx.getExecute());
|
Set<String> errorStates = Set.of(
|
||||||
|
AppsmithErrorCode.GIT_ROUTE_REDIS_DOWNLOAD_FAILED.getCode(),
|
||||||
|
AppsmithErrorCode.GIT_ROUTE_FS_CLEAN_UP_REQUIRED.getCode(),
|
||||||
|
AppsmithErrorCode.GIT_ROUTE_FS_OPS_NOT_REQUIRED.getCode(),
|
||||||
|
AppsmithErrorCode.GIT_ROUTE_METADATA_NOT_FOUND.getCode());
|
||||||
|
if (ctx.getError() == null || errorStates.contains(ctx.getError().getAppErrorCode())) {
|
||||||
|
return Mono.just(ctx.getExecute());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mono.error(ctx.getError());
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Helpers: Git Route
|
* Helpers: Git Route
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Extracts field from join point
|
/**
|
||||||
|
* Extracts a named parameter value from a {@link ProceedingJoinPoint}.
|
||||||
|
*
|
||||||
|
* @param jp the join point
|
||||||
|
* @param target the target parameter name to extract
|
||||||
|
* @return stringified value of the parameter if found, otherwise null
|
||||||
|
*/
|
||||||
private static String extractFieldValue(ProceedingJoinPoint jp, String target) {
|
private static String extractFieldValue(ProceedingJoinPoint jp, String target) {
|
||||||
String[] names = ((CodeSignature) jp.getSignature()).getParameterNames();
|
String[] names = ((CodeSignature) jp.getSignature()).getParameterNames();
|
||||||
Object[] values = jp.getArgs();
|
Object[] values = jp.getArgs();
|
||||||
|
|
@ -338,7 +692,13 @@ public class GitRouteAspect {
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets context field value
|
/**
|
||||||
|
* Uses reflection to set a field on the {@link Context} object by name.
|
||||||
|
*
|
||||||
|
* @param ctx the FSM context
|
||||||
|
* @param fieldName the field name to set
|
||||||
|
* @param value the value to assign
|
||||||
|
*/
|
||||||
private static void setContextField(Context ctx, String fieldName, Object value) {
|
private static void setContextField(Context ctx, String fieldName, Object value) {
|
||||||
try {
|
try {
|
||||||
var field = ctx.getClass().getDeclaredField(fieldName);
|
var field = ctx.getClass().getDeclaredField(fieldName);
|
||||||
|
|
@ -354,7 +714,15 @@ public class GitRouteAspect {
|
||||||
* Reference: SshTransportConfigCallback.java
|
* Reference: SshTransportConfigCallback.java
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Processes SSH private key
|
/**
|
||||||
|
* Process an SSH private key that may be in PEM or Base64 PKCS8 form and return a normalized encoded key
|
||||||
|
* string suitable for downstream usage.
|
||||||
|
*
|
||||||
|
* @param privateKey the private key content
|
||||||
|
* @param publicKey the corresponding public key (used to determine algorithm)
|
||||||
|
* @return normalized and encoded private key string
|
||||||
|
* @throws Exception if parsing or key generation fails
|
||||||
|
*/
|
||||||
private static String processPrivateKey(String privateKey, String publicKey) throws Exception {
|
private static String processPrivateKey(String privateKey, String publicKey) throws Exception {
|
||||||
String[] splitKeys = privateKey.split("-----.*-----\n");
|
String[] splitKeys = privateKey.split("-----.*-----\n");
|
||||||
return splitKeys.length > 1
|
return splitKeys.length > 1
|
||||||
|
|
@ -362,7 +730,14 @@ public class GitRouteAspect {
|
||||||
: handleBase64Format(privateKey, publicKey);
|
: handleBase64Format(privateKey, publicKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles PEM format key
|
/**
|
||||||
|
* Handle an OpenSSH PEM-formatted private key and return a Base64-encoded PKCS8 representation.
|
||||||
|
*
|
||||||
|
* @param privateKey the PEM-formatted private key
|
||||||
|
* @param publicKey the public key to infer algorithm
|
||||||
|
* @return Base64-encoded PKCS8 private key
|
||||||
|
* @throws Exception if parsing or key generation fails
|
||||||
|
*/
|
||||||
private static String handlePemFormat(String privateKey, String publicKey) throws Exception {
|
private static String handlePemFormat(String privateKey, String publicKey) throws Exception {
|
||||||
byte[] content =
|
byte[] content =
|
||||||
new PemReader(new StringReader(privateKey)).readPemObject().getContent();
|
new PemReader(new StringReader(privateKey)).readPemObject().getContent();
|
||||||
|
|
@ -372,7 +747,14 @@ public class GitRouteAspect {
|
||||||
return Base64.getEncoder().encodeToString(generatedPrivateKey.getEncoded());
|
return Base64.getEncoder().encodeToString(generatedPrivateKey.getEncoded());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles Base64 format key
|
/**
|
||||||
|
* Handle a Base64-encoded PKCS8 private key blob and return a normalized, formatted key string.
|
||||||
|
*
|
||||||
|
* @param privateKey the Base64-encoded PKCS8 private key
|
||||||
|
* @param publicKey the public key to infer algorithm
|
||||||
|
* @return normalized, formatted private key string
|
||||||
|
* @throws Exception if decoding or key generation fails
|
||||||
|
*/
|
||||||
private static String handleBase64Format(String privateKey, String publicKey) throws Exception {
|
private static String handleBase64Format(String privateKey, String publicKey) throws Exception {
|
||||||
PKCS8EncodedKeySpec privateKeySpec =
|
PKCS8EncodedKeySpec privateKeySpec =
|
||||||
new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey));
|
new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey));
|
||||||
|
|
@ -380,13 +762,24 @@ public class GitRouteAspect {
|
||||||
return formatPrivateKey(Base64.getEncoder().encodeToString(generatedPrivateKey.getEncoded()));
|
return formatPrivateKey(Base64.getEncoder().encodeToString(generatedPrivateKey.getEncoded()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets key factory for algorithm
|
/**
|
||||||
|
* Get a {@link KeyFactory} instance for the algorithm implied by the provided public key.
|
||||||
|
*
|
||||||
|
* @param publicKey the public key whose prefix indicates algorithm
|
||||||
|
* @return a {@link KeyFactory} for RSA or ECDSA
|
||||||
|
* @throws Exception if the algorithm or provider is unavailable
|
||||||
|
*/
|
||||||
private static KeyFactory getKeyFactory(String publicKey) throws Exception {
|
private static KeyFactory getKeyFactory(String publicKey) throws Exception {
|
||||||
String algo = publicKey.startsWith("ssh-rsa") ? "RSA" : "ECDSA";
|
String algo = publicKey.startsWith("ssh-rsa") ? "RSA" : "ECDSA";
|
||||||
return KeyFactory.getInstance(algo, new BouncyCastleProvider());
|
return KeyFactory.getInstance(algo, new BouncyCastleProvider());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Formats private key string
|
/**
|
||||||
|
* Format a Base64-encoded PKCS8 private key into a standard PEM wrapper string.
|
||||||
|
*
|
||||||
|
* @param privateKey the Base64-encoded PKCS8 private key
|
||||||
|
* @return a PEM-wrapped private key string
|
||||||
|
*/
|
||||||
private static String formatPrivateKey(String privateKey) {
|
private static String formatPrivateKey(String privateKey) {
|
||||||
return "-----BEGIN PRIVATE KEY-----\n" + privateKey + "\n-----END PRIVATE KEY-----";
|
return "-----BEGIN PRIVATE KEY-----\n" + privateKey + "\n-----END PRIVATE KEY-----";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,43 +2,44 @@
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
START([START]) --> ARTIFACT
|
flowchart TD
|
||||||
|
START(["START"]) --> ROUTE_FILTER["ROUTE_FILTER"]
|
||||||
ARTIFACT -->|SUCCESS| PARENT
|
ROUTE_FILTER -- SUCCESS --> ARTIFACT["ARTIFACT"]
|
||||||
ARTIFACT -->|FAIL| RESULT
|
ROUTE_FILTER -- FAIL --> UNROUTED_EXECUTION["UNROUTED_EXECUTION"]
|
||||||
|
ARTIFACT -- SUCCESS --> METADATA_FILTER["METADATA_FILTER"]
|
||||||
PARENT -->|SUCCESS| GIT_META
|
ARTIFACT -- FAIL --> RESULT["RESULT"]
|
||||||
PARENT -->|FAIL| RESULT
|
UNROUTED_EXECUTION -- SUCCESS or FAIL --> RESULT
|
||||||
|
METADATA_FILTER -- SUCCESS --> PARENT["PARENT"]
|
||||||
GIT_META -->|SUCCESS| REPO_KEY
|
METADATA_FILTER -- FAIL --> UNROUTED_EXECUTION
|
||||||
GIT_META -->|FAIL| RESULT
|
PARENT -- SUCCESS --> GIT_META["GIT_META"]
|
||||||
|
PARENT -- FAIL --> RESULT
|
||||||
REPO_KEY -->|SUCCESS| LOCK_KEY
|
GIT_META -- SUCCESS --> REPO_KEY["REPO_KEY"]
|
||||||
REPO_KEY -->|FAIL| RESULT
|
GIT_META -- FAIL --> RESULT
|
||||||
|
REPO_KEY -- SUCCESS --> LOCK_KEY["LOCK_KEY"]
|
||||||
LOCK_KEY -->|SUCCESS| LOCK
|
REPO_KEY -- FAIL --> RESULT
|
||||||
LOCK_KEY -->|FAIL| RESULT
|
LOCK_KEY -- SUCCESS --> LOCK["LOCK"]
|
||||||
|
LOCK_KEY -- FAIL --> RESULT
|
||||||
LOCK -->|SUCCESS| GIT_PROFILE
|
LOCK -- SUCCESS --> GIT_PROFILE["GIT_PROFILE"]
|
||||||
LOCK -->|FAIL| RESULT
|
LOCK -- FAIL --> RESULT
|
||||||
|
GIT_PROFILE -- SUCCESS --> GIT_AUTH["GIT_AUTH"]
|
||||||
GIT_PROFILE -->|SUCCESS| GIT_AUTH
|
GIT_PROFILE -- FAIL --> UNLOCK["UNLOCK"]
|
||||||
GIT_PROFILE -->|FAIL| UNLOCK
|
GIT_AUTH -- SUCCESS --> GIT_KEY["GIT_KEY"]
|
||||||
|
GIT_AUTH -- FAIL --> UNLOCK
|
||||||
GIT_AUTH -->|SUCCESS| GIT_KEY
|
GIT_KEY -- SUCCESS --> REPO_PATH["REPO_PATH"]
|
||||||
GIT_AUTH -->|FAIL| UNLOCK
|
GIT_KEY -- FAIL --> UNLOCK
|
||||||
|
REPO_PATH -- SUCCESS --> DOWNLOAD["DOWNLOAD"]
|
||||||
GIT_KEY -->|SUCCESS| REPO_PATH
|
REPO_PATH -- FAIL --> UNLOCK
|
||||||
GIT_KEY -->|FAIL| UNLOCK
|
DOWNLOAD -- SUCCESS --> EXECUTE["EXECUTE"]
|
||||||
|
DOWNLOAD -- FAIL --> FETCH_BRANCHES["FETCH_BRANCHES"]
|
||||||
REPO_PATH -->|SUCCESS| DOWNLOAD
|
FETCH_BRANCHES -- SUCCESS --> CLONE["CLONE"]
|
||||||
REPO_PATH -->|FAIL| UNLOCK
|
FETCH_BRANCHES -- FAIL --> UNLOCK
|
||||||
|
CLONE -- SUCCESS --> EXECUTE
|
||||||
DOWNLOAD -->|SUCCESS| EXECUTE
|
CLONE -- FAIL --> UNLOCK
|
||||||
DOWNLOAD -->|FAIL| UNLOCK
|
EXECUTE -- SUCCESS or FAIL --> CLEAN_UP_FILTER["CLEAN_UP_FILTER"]
|
||||||
|
CLEAN_UP_FILTER -- SUCCESS --> UPLOAD["UPLOAD"]
|
||||||
EXECUTE -->|SUCCESS or FAIL| UPLOAD
|
CLEAN_UP_FILTER -- FAIL --> CLEAN_UP["CLEAN_UP"]
|
||||||
UPLOAD -->|SUCCESS or FAIL| UNLOCK
|
UPLOAD -- SUCCESS or FAIL --> UNLOCK
|
||||||
UNLOCK -->|SUCCESS or FAIL| RESULT
|
CLEAN_UP -- SUCCESS or FAIL --> UNLOCK
|
||||||
RESULT --> DONE([DONE])
|
UNLOCK -- SUCCESS or FAIL --> RESULT
|
||||||
|
RESULT --> DONE(["DONE"])
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1020,6 +1020,38 @@ public enum AppsmithError {
|
||||||
"Insufficient password strength",
|
"Insufficient password strength",
|
||||||
ErrorType.ARGUMENT_ERROR,
|
ErrorType.ARGUMENT_ERROR,
|
||||||
null),
|
null),
|
||||||
|
GIT_ROUTE_METADATA_NOT_FOUND(
|
||||||
|
404,
|
||||||
|
AppsmithErrorCode.GIT_ROUTE_METADATA_NOT_FOUND.getCode(),
|
||||||
|
"Artifact metadata not found for Git route: {0}",
|
||||||
|
AppsmithErrorAction.DEFAULT,
|
||||||
|
"Git route metadata not found",
|
||||||
|
ErrorType.GIT_CONFIGURATION_ERROR,
|
||||||
|
null),
|
||||||
|
GIT_ROUTE_FS_OPS_NOT_REQUIRED(
|
||||||
|
404,
|
||||||
|
AppsmithErrorCode.GIT_ROUTE_FS_OPS_NOT_REQUIRED.getCode(),
|
||||||
|
"Endpoint : {0}, does not require File system operation",
|
||||||
|
AppsmithErrorAction.DEFAULT,
|
||||||
|
"Git FS operation not required",
|
||||||
|
ErrorType.GIT_CONFIGURATION_ERROR,
|
||||||
|
null),
|
||||||
|
GIT_ROUTE_FS_CLEAN_UP_REQUIRED(
|
||||||
|
404,
|
||||||
|
AppsmithErrorCode.GIT_ROUTE_FS_CLEAN_UP_REQUIRED.getCode(),
|
||||||
|
"Endpoint : {0}, requires FS and redis cleanup",
|
||||||
|
AppsmithErrorAction.DEFAULT,
|
||||||
|
"Git FS operation not required",
|
||||||
|
ErrorType.INTERNAL_ERROR,
|
||||||
|
null),
|
||||||
|
GIT_ROUTE_REDIS_DOWNLOAD_FAILED(
|
||||||
|
500,
|
||||||
|
AppsmithErrorCode.GIT_ROUTE_REDIS_DOWNLOAD_FAILED.getCode(),
|
||||||
|
"Download of git repository from redis failed. Error: {0}",
|
||||||
|
AppsmithErrorAction.DEFAULT,
|
||||||
|
"Git repository download failed",
|
||||||
|
ErrorType.GIT_CONFIGURATION_ERROR,
|
||||||
|
null),
|
||||||
GIT_ROUTE_HANDLER_NOT_FOUND(
|
GIT_ROUTE_HANDLER_NOT_FOUND(
|
||||||
500,
|
500,
|
||||||
AppsmithErrorCode.GIT_ROUTE_HANDLER_NOT_FOUND.getCode(),
|
AppsmithErrorCode.GIT_ROUTE_HANDLER_NOT_FOUND.getCode(),
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,10 @@ public enum AppsmithErrorCode {
|
||||||
GIT_ROUTE_CONTEXT_BUILD_ERROR("AE-GIT-5007", "Git route context build error"),
|
GIT_ROUTE_CONTEXT_BUILD_ERROR("AE-GIT-5007", "Git route context build error"),
|
||||||
GIT_ROUTE_ARTIFACT_NOT_FOUND("AE-GIT-5008", "Git route artifact not found"),
|
GIT_ROUTE_ARTIFACT_NOT_FOUND("AE-GIT-5008", "Git route artifact not found"),
|
||||||
GIT_ROUTE_INVALID_PRIVATE_KEY("AE-GIT-5009", "Git route invalid private key"),
|
GIT_ROUTE_INVALID_PRIVATE_KEY("AE-GIT-5009", "Git route invalid private key"),
|
||||||
|
GIT_ROUTE_METADATA_NOT_FOUND("AE-GIT-5010", "Git route metadata not found"),
|
||||||
|
GIT_ROUTE_FS_OPS_NOT_REQUIRED("AE-GIT-5011", "Git FS operation not required"),
|
||||||
|
GIT_ROUTE_REDIS_DOWNLOAD_FAILED("AE-GIT-5012", "Git redis download failed"),
|
||||||
|
GIT_ROUTE_FS_CLEAN_UP_REQUIRED("AE-GIT-5015", "Git FS clean up required"),
|
||||||
|
|
||||||
DATASOURCE_CONNECTION_RATE_LIMIT_BLOCKING_FAILED(
|
DATASOURCE_CONNECTION_RATE_LIMIT_BLOCKING_FAILED(
|
||||||
"AE-TMR-4031", "Rate limit exhausted, blocking the host name failed"),
|
"AE-TMR-4031", "Rate limit exhausted, blocking the host name failed"),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
package com.appsmith.server.git.constants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common Git route operations for Application and Package controllers.
|
||||||
|
*
|
||||||
|
* Grouping:
|
||||||
|
* - Common operations are listed first.
|
||||||
|
* - Controller-specific operations are separated by comment delimiters.
|
||||||
|
*/
|
||||||
|
public enum GitRouteOperation {
|
||||||
|
|
||||||
|
// Common operations
|
||||||
|
COMMIT(true),
|
||||||
|
CREATE_REF(true),
|
||||||
|
CHECKOUT_REF(true),
|
||||||
|
DISCONNECT(true, true),
|
||||||
|
PULL(true),
|
||||||
|
STATUS(true),
|
||||||
|
FETCH_REMOTE_CHANGES(true),
|
||||||
|
MERGE(true),
|
||||||
|
MERGE_STATUS(true),
|
||||||
|
DELETE_REF(true),
|
||||||
|
DISCARD_CHANGES(true),
|
||||||
|
LIST_REFS(true),
|
||||||
|
AUTO_COMMIT(true),
|
||||||
|
|
||||||
|
// whitelisted ones
|
||||||
|
|
||||||
|
METADATA(false),
|
||||||
|
CONNECT(false),
|
||||||
|
GET_PROTECTED_BRANCHES(false),
|
||||||
|
UPDATE_PROTECTED_BRANCHES(false),
|
||||||
|
GET_SSH_KEY(false),
|
||||||
|
GENERATE_SSH_KEYPAIR(false),
|
||||||
|
GET_AUTO_COMMIT_PROGRESS(false),
|
||||||
|
TOGGLE_AUTO_COMMIT(false),
|
||||||
|
|
||||||
|
// EE specific features
|
||||||
|
|
||||||
|
SET_DEFAULT_BRANCH(true),
|
||||||
|
DEPLOY(true),
|
||||||
|
|
||||||
|
TOGGLE_AUTO_DEPLOYMENT(false),
|
||||||
|
GENERATE_GIT_TOKEN(false),
|
||||||
|
|
||||||
|
// Package specific
|
||||||
|
PRE_TAG(true),
|
||||||
|
PUBLISH(true),
|
||||||
|
;
|
||||||
|
|
||||||
|
private final boolean requiresGitOperation;
|
||||||
|
private final boolean gitCleanUp;
|
||||||
|
|
||||||
|
GitRouteOperation() {
|
||||||
|
this(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
GitRouteOperation(boolean requiresGitOperation) {
|
||||||
|
this.requiresGitOperation = requiresGitOperation;
|
||||||
|
this.gitCleanUp = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GitRouteOperation(boolean requiresGitOperation, boolean gitCleanUp) {
|
||||||
|
this.requiresGitOperation = requiresGitOperation;
|
||||||
|
this.gitCleanUp = gitCleanUp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean requiresGitOperation() {
|
||||||
|
return requiresGitOperation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean gitCleanUp() {
|
||||||
|
return gitCleanUp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,7 @@ import com.appsmith.server.dtos.ResponseDTO;
|
||||||
import com.appsmith.server.git.autocommit.AutoCommitService;
|
import com.appsmith.server.git.autocommit.AutoCommitService;
|
||||||
import com.appsmith.server.git.central.CentralGitService;
|
import com.appsmith.server.git.central.CentralGitService;
|
||||||
import com.appsmith.server.git.central.GitType;
|
import com.appsmith.server.git.central.GitType;
|
||||||
|
import com.appsmith.server.git.constants.GitRouteOperation;
|
||||||
import com.fasterxml.jackson.annotation.JsonView;
|
import com.fasterxml.jackson.annotation.JsonView;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
@ -60,7 +61,10 @@ public class GitApplicationControllerCE {
|
||||||
|
|
||||||
@JsonView({Views.Metadata.class})
|
@JsonView({Views.Metadata.class})
|
||||||
@GetMapping("/{baseApplicationId}/metadata")
|
@GetMapping("/{baseApplicationId}/metadata")
|
||||||
@GitRoute(fieldName = "baseApplicationId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "baseApplicationId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.METADATA)
|
||||||
public Mono<ResponseDTO<GitArtifactMetadata>> getGitMetadata(@PathVariable String baseApplicationId) {
|
public Mono<ResponseDTO<GitArtifactMetadata>> getGitMetadata(@PathVariable String baseApplicationId) {
|
||||||
return centralGitService
|
return centralGitService
|
||||||
.getGitArtifactMetadata(baseApplicationId, ARTIFACT_TYPE)
|
.getGitArtifactMetadata(baseApplicationId, ARTIFACT_TYPE)
|
||||||
|
|
@ -69,7 +73,10 @@ public class GitApplicationControllerCE {
|
||||||
|
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@PostMapping("/{applicationId}/connect")
|
@PostMapping("/{applicationId}/connect")
|
||||||
@GitRoute(fieldName = "applicationId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "applicationId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.CONNECT)
|
||||||
public Mono<ResponseDTO<? extends Artifact>> connectApplicationToRemoteRepo(
|
public Mono<ResponseDTO<? extends Artifact>> connectApplicationToRemoteRepo(
|
||||||
@PathVariable String applicationId,
|
@PathVariable String applicationId,
|
||||||
@RequestBody GitConnectDTO gitConnectDTO,
|
@RequestBody GitConnectDTO gitConnectDTO,
|
||||||
|
|
@ -82,7 +89,10 @@ public class GitApplicationControllerCE {
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@PostMapping("/{branchedApplicationId}/commit")
|
@PostMapping("/{branchedApplicationId}/commit")
|
||||||
@ResponseStatus(HttpStatus.CREATED)
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
@GitRoute(fieldName = "branchedApplicationId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "branchedApplicationId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.COMMIT)
|
||||||
public Mono<ResponseDTO<String>> commit(
|
public Mono<ResponseDTO<String>> commit(
|
||||||
@RequestBody CommitDTO commitDTO, @PathVariable String branchedApplicationId) {
|
@RequestBody CommitDTO commitDTO, @PathVariable String branchedApplicationId) {
|
||||||
log.info("Going to commit branchedApplicationId {}", branchedApplicationId);
|
log.info("Going to commit branchedApplicationId {}", branchedApplicationId);
|
||||||
|
|
@ -94,7 +104,10 @@ public class GitApplicationControllerCE {
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@PostMapping("/{referencedApplicationId}/create-ref")
|
@PostMapping("/{referencedApplicationId}/create-ref")
|
||||||
@ResponseStatus(HttpStatus.CREATED)
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
@GitRoute(fieldName = "referencedApplicationId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "referencedApplicationId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.CREATE_REF)
|
||||||
public Mono<ResponseDTO<? extends Artifact>> createReference(
|
public Mono<ResponseDTO<? extends Artifact>> createReference(
|
||||||
@PathVariable String referencedApplicationId,
|
@PathVariable String referencedApplicationId,
|
||||||
@RequestHeader(name = FieldName.BRANCH_NAME, required = false) String srcBranch,
|
@RequestHeader(name = FieldName.BRANCH_NAME, required = false) String srcBranch,
|
||||||
|
|
@ -110,7 +123,10 @@ public class GitApplicationControllerCE {
|
||||||
|
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@PostMapping("/{referencedApplicationId}/checkout-ref")
|
@PostMapping("/{referencedApplicationId}/checkout-ref")
|
||||||
@GitRoute(fieldName = "referencedApplicationId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "referencedApplicationId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.CHECKOUT_REF)
|
||||||
public Mono<ResponseDTO<? extends Artifact>> checkoutReference(
|
public Mono<ResponseDTO<? extends Artifact>> checkoutReference(
|
||||||
@PathVariable String referencedApplicationId, @RequestBody GitRefDTO gitRefDTO) {
|
@PathVariable String referencedApplicationId, @RequestBody GitRefDTO gitRefDTO) {
|
||||||
return centralGitService
|
return centralGitService
|
||||||
|
|
@ -120,7 +136,10 @@ public class GitApplicationControllerCE {
|
||||||
|
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@PostMapping("/{branchedApplicationId}/disconnect")
|
@PostMapping("/{branchedApplicationId}/disconnect")
|
||||||
@GitRoute(fieldName = "branchedApplicationId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "branchedApplicationId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.DISCONNECT)
|
||||||
public Mono<ResponseDTO<? extends Artifact>> disconnectFromRemote(@PathVariable String branchedApplicationId) {
|
public Mono<ResponseDTO<? extends Artifact>> disconnectFromRemote(@PathVariable String branchedApplicationId) {
|
||||||
log.info("Going to remove the remoteUrl for application {}", branchedApplicationId);
|
log.info("Going to remove the remoteUrl for application {}", branchedApplicationId);
|
||||||
return centralGitService
|
return centralGitService
|
||||||
|
|
@ -130,7 +149,10 @@ public class GitApplicationControllerCE {
|
||||||
|
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@GetMapping("/{branchedApplicationId}/pull")
|
@GetMapping("/{branchedApplicationId}/pull")
|
||||||
@GitRoute(fieldName = "branchedApplicationId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "branchedApplicationId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.PULL)
|
||||||
public Mono<ResponseDTO<GitPullDTO>> pull(@PathVariable String branchedApplicationId) {
|
public Mono<ResponseDTO<GitPullDTO>> pull(@PathVariable String branchedApplicationId) {
|
||||||
log.info("Going to pull the latest for branchedApplicationId {}", branchedApplicationId);
|
log.info("Going to pull the latest for branchedApplicationId {}", branchedApplicationId);
|
||||||
return centralGitService
|
return centralGitService
|
||||||
|
|
@ -140,7 +162,10 @@ public class GitApplicationControllerCE {
|
||||||
|
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@GetMapping("/{branchedApplicationId}/status")
|
@GetMapping("/{branchedApplicationId}/status")
|
||||||
@GitRoute(fieldName = "branchedApplicationId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "branchedApplicationId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.STATUS)
|
||||||
public Mono<ResponseDTO<GitStatusDTO>> getStatus(
|
public Mono<ResponseDTO<GitStatusDTO>> getStatus(
|
||||||
@PathVariable String branchedApplicationId,
|
@PathVariable String branchedApplicationId,
|
||||||
@RequestParam(required = false, defaultValue = "true") Boolean compareRemote) {
|
@RequestParam(required = false, defaultValue = "true") Boolean compareRemote) {
|
||||||
|
|
@ -152,7 +177,10 @@ public class GitApplicationControllerCE {
|
||||||
|
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@GetMapping("/{referencedApplicationId}/fetch/remote")
|
@GetMapping("/{referencedApplicationId}/fetch/remote")
|
||||||
@GitRoute(fieldName = "referencedApplicationId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "referencedApplicationId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.FETCH_REMOTE_CHANGES)
|
||||||
public Mono<ResponseDTO<BranchTrackingStatus>> fetchRemoteChanges(
|
public Mono<ResponseDTO<BranchTrackingStatus>> fetchRemoteChanges(
|
||||||
@PathVariable String referencedApplicationId,
|
@PathVariable String referencedApplicationId,
|
||||||
@RequestHeader(required = false, defaultValue = "branch") RefType refType) {
|
@RequestHeader(required = false, defaultValue = "branch") RefType refType) {
|
||||||
|
|
@ -164,7 +192,10 @@ public class GitApplicationControllerCE {
|
||||||
|
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@PostMapping("/{branchedApplicationId}/merge")
|
@PostMapping("/{branchedApplicationId}/merge")
|
||||||
@GitRoute(fieldName = "branchedApplicationId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "branchedApplicationId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.MERGE)
|
||||||
public Mono<ResponseDTO<MergeStatusDTO>> merge(
|
public Mono<ResponseDTO<MergeStatusDTO>> merge(
|
||||||
@PathVariable String branchedApplicationId, @RequestBody GitMergeDTO gitMergeDTO) {
|
@PathVariable String branchedApplicationId, @RequestBody GitMergeDTO gitMergeDTO) {
|
||||||
log.debug(
|
log.debug(
|
||||||
|
|
@ -179,7 +210,10 @@ public class GitApplicationControllerCE {
|
||||||
|
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@PostMapping("/{branchedApplicationId}/merge/status")
|
@PostMapping("/{branchedApplicationId}/merge/status")
|
||||||
@GitRoute(fieldName = "branchedApplicationId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "branchedApplicationId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.MERGE_STATUS)
|
||||||
public Mono<ResponseDTO<MergeStatusDTO>> mergeStatus(
|
public Mono<ResponseDTO<MergeStatusDTO>> mergeStatus(
|
||||||
@PathVariable String branchedApplicationId, @RequestBody GitMergeDTO gitMergeDTO) {
|
@PathVariable String branchedApplicationId, @RequestBody GitMergeDTO gitMergeDTO) {
|
||||||
log.info(
|
log.info(
|
||||||
|
|
@ -194,7 +228,10 @@ public class GitApplicationControllerCE {
|
||||||
|
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@DeleteMapping("/{baseArtifactId}/ref")
|
@DeleteMapping("/{baseArtifactId}/ref")
|
||||||
@GitRoute(fieldName = "baseArtifactId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "baseArtifactId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.DELETE_REF)
|
||||||
public Mono<ResponseDTO<? extends Artifact>> deleteBranch(
|
public Mono<ResponseDTO<? extends Artifact>> deleteBranch(
|
||||||
@PathVariable String baseArtifactId,
|
@PathVariable String baseArtifactId,
|
||||||
@RequestParam String refName,
|
@RequestParam String refName,
|
||||||
|
|
@ -207,7 +244,10 @@ public class GitApplicationControllerCE {
|
||||||
|
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@PutMapping("/{branchedApplicationId}/discard")
|
@PutMapping("/{branchedApplicationId}/discard")
|
||||||
@GitRoute(fieldName = "branchedApplicationId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "branchedApplicationId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.DISCARD_CHANGES)
|
||||||
public Mono<ResponseDTO<? extends Artifact>> discardChanges(@PathVariable String branchedApplicationId) {
|
public Mono<ResponseDTO<? extends Artifact>> discardChanges(@PathVariable String branchedApplicationId) {
|
||||||
log.info("Going to discard changes for branchedApplicationId {}", branchedApplicationId);
|
log.info("Going to discard changes for branchedApplicationId {}", branchedApplicationId);
|
||||||
return centralGitService
|
return centralGitService
|
||||||
|
|
@ -217,7 +257,10 @@ public class GitApplicationControllerCE {
|
||||||
|
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@PostMapping("/{baseArtifactId}/protected-branches")
|
@PostMapping("/{baseArtifactId}/protected-branches")
|
||||||
@GitRoute(fieldName = "baseArtifactId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "baseArtifactId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.UPDATE_PROTECTED_BRANCHES)
|
||||||
public Mono<ResponseDTO<List<String>>> updateProtectedBranches(
|
public Mono<ResponseDTO<List<String>>> updateProtectedBranches(
|
||||||
@PathVariable String baseArtifactId,
|
@PathVariable String baseArtifactId,
|
||||||
@RequestBody @Valid BranchProtectionRequestDTO branchProtectionRequestDTO) {
|
@RequestBody @Valid BranchProtectionRequestDTO branchProtectionRequestDTO) {
|
||||||
|
|
@ -228,7 +271,10 @@ public class GitApplicationControllerCE {
|
||||||
|
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@GetMapping("/{baseArtifactId}/protected-branches")
|
@GetMapping("/{baseArtifactId}/protected-branches")
|
||||||
@GitRoute(fieldName = "baseArtifactId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "baseArtifactId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.GET_PROTECTED_BRANCHES)
|
||||||
public Mono<ResponseDTO<List<String>>> getProtectedBranches(@PathVariable String baseArtifactId) {
|
public Mono<ResponseDTO<List<String>>> getProtectedBranches(@PathVariable String baseArtifactId) {
|
||||||
return centralGitService
|
return centralGitService
|
||||||
.getProtectedBranches(baseArtifactId, ARTIFACT_TYPE)
|
.getProtectedBranches(baseArtifactId, ARTIFACT_TYPE)
|
||||||
|
|
@ -237,7 +283,10 @@ public class GitApplicationControllerCE {
|
||||||
|
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@PostMapping("/{branchedApplicationId}/auto-commit")
|
@PostMapping("/{branchedApplicationId}/auto-commit")
|
||||||
@GitRoute(fieldName = "branchedApplicationId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "branchedApplicationId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.AUTO_COMMIT)
|
||||||
public Mono<ResponseDTO<AutoCommitResponseDTO>> autoCommitApplication(@PathVariable String branchedApplicationId) {
|
public Mono<ResponseDTO<AutoCommitResponseDTO>> autoCommitApplication(@PathVariable String branchedApplicationId) {
|
||||||
return autoCommitService
|
return autoCommitService
|
||||||
.autoCommitApplication(branchedApplicationId)
|
.autoCommitApplication(branchedApplicationId)
|
||||||
|
|
@ -246,7 +295,10 @@ public class GitApplicationControllerCE {
|
||||||
|
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@GetMapping("/{baseApplicationId}/auto-commit/progress")
|
@GetMapping("/{baseApplicationId}/auto-commit/progress")
|
||||||
@GitRoute(fieldName = "baseApplicationId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "baseApplicationId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.GET_AUTO_COMMIT_PROGRESS)
|
||||||
public Mono<ResponseDTO<AutoCommitResponseDTO>> getAutoCommitProgress(
|
public Mono<ResponseDTO<AutoCommitResponseDTO>> getAutoCommitProgress(
|
||||||
@PathVariable String baseApplicationId,
|
@PathVariable String baseApplicationId,
|
||||||
@RequestHeader(name = FieldName.BRANCH_NAME, required = false) String branchName) {
|
@RequestHeader(name = FieldName.BRANCH_NAME, required = false) String branchName) {
|
||||||
|
|
@ -257,7 +309,10 @@ public class GitApplicationControllerCE {
|
||||||
|
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@PatchMapping("/{baseArtifactId}/auto-commit/toggle")
|
@PatchMapping("/{baseArtifactId}/auto-commit/toggle")
|
||||||
@GitRoute(fieldName = "baseArtifactId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "baseArtifactId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.TOGGLE_AUTO_COMMIT)
|
||||||
public Mono<ResponseDTO<Boolean>> toggleAutoCommitEnabled(@PathVariable String baseArtifactId) {
|
public Mono<ResponseDTO<Boolean>> toggleAutoCommitEnabled(@PathVariable String baseArtifactId) {
|
||||||
return centralGitService
|
return centralGitService
|
||||||
.toggleAutoCommitEnabled(baseArtifactId, ARTIFACT_TYPE)
|
.toggleAutoCommitEnabled(baseArtifactId, ARTIFACT_TYPE)
|
||||||
|
|
@ -266,7 +321,10 @@ public class GitApplicationControllerCE {
|
||||||
|
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@GetMapping("/{branchedApplicationId}/refs")
|
@GetMapping("/{branchedApplicationId}/refs")
|
||||||
@GitRoute(fieldName = "branchedApplicationId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "branchedApplicationId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.LIST_REFS)
|
||||||
public Mono<ResponseDTO<List<GitRefDTO>>> getReferences(
|
public Mono<ResponseDTO<List<GitRefDTO>>> getReferences(
|
||||||
@PathVariable String branchedApplicationId,
|
@PathVariable String branchedApplicationId,
|
||||||
@RequestParam(required = false, defaultValue = "branch") RefType refType,
|
@RequestParam(required = false, defaultValue = "branch") RefType refType,
|
||||||
|
|
@ -279,7 +337,10 @@ public class GitApplicationControllerCE {
|
||||||
|
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@GetMapping("/{branchedApplicationId}/ssh-keypair")
|
@GetMapping("/{branchedApplicationId}/ssh-keypair")
|
||||||
@GitRoute(fieldName = "branchedApplicationId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "branchedApplicationId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.GET_SSH_KEY)
|
||||||
public Mono<ResponseDTO<GitAuthDTO>> getSSHKey(@PathVariable String branchedApplicationId) {
|
public Mono<ResponseDTO<GitAuthDTO>> getSSHKey(@PathVariable String branchedApplicationId) {
|
||||||
return artifactService
|
return artifactService
|
||||||
.getSshKey(ARTIFACT_TYPE, branchedApplicationId)
|
.getSshKey(ARTIFACT_TYPE, branchedApplicationId)
|
||||||
|
|
@ -288,7 +349,10 @@ public class GitApplicationControllerCE {
|
||||||
|
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@PostMapping("/{branchedApplicationId}/ssh-keypair")
|
@PostMapping("/{branchedApplicationId}/ssh-keypair")
|
||||||
@GitRoute(fieldName = "branchedApplicationId", artifactType = ArtifactType.APPLICATION)
|
@GitRoute(
|
||||||
|
fieldName = "branchedApplicationId",
|
||||||
|
artifactType = ArtifactType.APPLICATION,
|
||||||
|
operation = GitRouteOperation.GENERATE_SSH_KEYPAIR)
|
||||||
public Mono<ResponseDTO<GitAuth>> generateSSHKeyPair(
|
public Mono<ResponseDTO<GitAuth>> generateSSHKeyPair(
|
||||||
@PathVariable String branchedApplicationId, @RequestParam(required = false) String keyType) {
|
@PathVariable String branchedApplicationId, @RequestParam(required = false) String keyType) {
|
||||||
return artifactService
|
return artifactService
|
||||||
|
|
|
||||||
|
|
@ -714,17 +714,25 @@ public class GitFSServiceCEImpl implements GitHandlingServiceCE {
|
||||||
GitArtifactHelper<?> gitArtifactHelper = gitArtifactHelperResolver.getArtifactHelper(artifactType);
|
GitArtifactHelper<?> gitArtifactHelper = gitArtifactHelperResolver.getArtifactHelper(artifactType);
|
||||||
Path repoSuffix = gitArtifactHelper.getRepoSuffixPath(workspaceId, baseArtifactId, repoName);
|
Path repoSuffix = gitArtifactHelper.getRepoSuffixPath(workspaceId, baseArtifactId, repoName);
|
||||||
|
|
||||||
if (gitServiceConfig.isGitInMemory()) {
|
|
||||||
return fsGitHandler.mergeBranch(
|
|
||||||
repoSuffix, gitMergeDTO.getSourceBranch(), gitMergeDTO.getDestinationBranch());
|
|
||||||
}
|
|
||||||
|
|
||||||
Mono<Boolean> keepWorkingDirChangesMono =
|
Mono<Boolean> keepWorkingDirChangesMono =
|
||||||
featureFlagService.check(FeatureFlagEnum.release_git_reset_optimization_enabled);
|
featureFlagService.check(FeatureFlagEnum.release_git_reset_optimization_enabled);
|
||||||
|
|
||||||
// At this point the assumption is that the repository has already checked out the destination branch
|
// At this point the assumption is that the repository has already checked out the destination branch
|
||||||
return keepWorkingDirChangesMono.flatMap(keepWorkingDirChanges -> fsGitHandler.mergeBranch(
|
return keepWorkingDirChangesMono.flatMap(keepWorkingDirChanges -> {
|
||||||
repoSuffix, gitMergeDTO.getSourceBranch(), gitMergeDTO.getDestinationBranch(), keepWorkingDirChanges));
|
if (gitServiceConfig.isGitInMemory()) {
|
||||||
|
return fsGitHandler.mergeBranch(
|
||||||
|
repoSuffix,
|
||||||
|
gitMergeDTO.getSourceBranch(),
|
||||||
|
gitMergeDTO.getDestinationBranch(),
|
||||||
|
keepWorkingDirChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fsGitHandler.mergeBranch(
|
||||||
|
repoSuffix,
|
||||||
|
gitMergeDTO.getSourceBranch(),
|
||||||
|
gitMergeDTO.getDestinationBranch(),
|
||||||
|
keepWorkingDirChanges);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,448 @@
|
||||||
|
package com.appsmith.server.aspect;
|
||||||
|
|
||||||
|
import com.appsmith.external.git.constants.ce.RefType;
|
||||||
|
import com.appsmith.server.annotations.GitRoute;
|
||||||
|
import com.appsmith.server.artifacts.gitRoute.GitRouteArtifact;
|
||||||
|
import com.appsmith.server.constants.ArtifactType;
|
||||||
|
import com.appsmith.server.domains.Application;
|
||||||
|
import com.appsmith.server.domains.Artifact;
|
||||||
|
import com.appsmith.server.domains.GitArtifactMetadata;
|
||||||
|
import com.appsmith.server.domains.GitAuth;
|
||||||
|
import com.appsmith.server.domains.GitProfile;
|
||||||
|
import com.appsmith.server.exceptions.AppsmithError;
|
||||||
|
import com.appsmith.server.exceptions.AppsmithException;
|
||||||
|
import com.appsmith.server.git.constants.GitRouteOperation;
|
||||||
|
import com.appsmith.server.services.GitArtifactHelper;
|
||||||
|
import io.micrometer.observation.ObservationRegistry;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.mockito.stubbing.Answer;
|
||||||
|
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class GitRouteAspectTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ReactiveRedisTemplate<String, String> redis;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private com.appsmith.server.git.utils.GitProfileUtils gitProfileUtils;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private com.appsmith.git.configurations.GitServiceConfig gitServiceConfig;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private GitRouteArtifact gitRouteArtifact;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private GitArtifactHelper<?> gitArtifactHelper;
|
||||||
|
|
||||||
|
private ObservationRegistry observationRegistry;
|
||||||
|
|
||||||
|
private GitRouteAspect aspect;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
observationRegistry = ObservationRegistry.create();
|
||||||
|
aspect = new GitRouteAspect(redis, gitProfileUtils, gitServiceConfig, gitRouteArtifact, observationRegistry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- Helpers ----------------------
|
||||||
|
|
||||||
|
private static Class<?> contextClass() throws Exception {
|
||||||
|
return Class.forName("com.appsmith.server.aspect.GitRouteAspect$Context");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object newContext() throws Exception {
|
||||||
|
Class<?> ctxClz = contextClass();
|
||||||
|
Constructor<?> ctor = ctxClz.getDeclaredConstructor();
|
||||||
|
ctor.setAccessible(true);
|
||||||
|
return ctor.newInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setCtx(Object ctx, String field, Object value) throws Exception {
|
||||||
|
Field f = ctx.getClass().getDeclaredField(field);
|
||||||
|
f.setAccessible(true);
|
||||||
|
f.set(ctx, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object getCtx(Object ctx, String field) throws Exception {
|
||||||
|
Field f = ctx.getClass().getDeclaredField(field);
|
||||||
|
f.setAccessible(true);
|
||||||
|
return f.get(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private <T> Mono<T> invokeMono(String methodName, Class<?>[] paramTypes, Object... args) throws Exception {
|
||||||
|
Method m = GitRouteAspect.class.getDeclaredMethod(methodName, paramTypes);
|
||||||
|
m.setAccessible(true);
|
||||||
|
return (Mono<T>) m.invoke(aspect, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<Boolean> invokeSetLock(String key, String command) throws Exception {
|
||||||
|
return invokeMono("setLock", new Class<?>[] {String.class, String.class}, key, command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<Boolean> invokeAddLockWithRetry(String key, String command) throws Exception {
|
||||||
|
return invokeMono("addLockWithRetry", new Class<?>[] {String.class, String.class}, key, command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<?> invokeRouteFilter(Object ctx) throws Exception {
|
||||||
|
return invokeMono("routeFilter", new Class<?>[] {contextClass()}, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<?> invokeCleanUpFilter(Object ctx) throws Exception {
|
||||||
|
return invokeMono("cleanUpFilter", new Class<?>[] {contextClass()}, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<?> invokeMetadataFilter(Object ctx) throws Exception {
|
||||||
|
return invokeMono("metadataFilter", new Class<?>[] {contextClass()}, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<?> invokeGitProfile(Object ctx) throws Exception {
|
||||||
|
return invokeMono("gitProfile", new Class<?>[] {contextClass()}, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<?> invokeGitAuth(Object ctx) throws Exception {
|
||||||
|
return invokeMono("gitAuth", new Class<?>[] {contextClass()}, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<?> invokeRepoKey(Object ctx) throws Exception {
|
||||||
|
return invokeMono("repoKey", new Class<?>[] {contextClass()}, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<?> invokeLockKey(Object ctx) throws Exception {
|
||||||
|
return invokeMono("lockKey", new Class<?>[] {contextClass()}, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<?> invokeGetLocalBranches(Object ctx) throws Exception {
|
||||||
|
return invokeMono("getLocalBranches", new Class<?>[] {contextClass()}, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<?> invokeExecute(Object ctx) throws Exception {
|
||||||
|
return invokeMono("execute", new Class<?>[] {contextClass()}, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<?> invokeUnlock(Object ctx) throws Exception {
|
||||||
|
return invokeMono("unlock", new Class<?>[] {contextClass()}, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<?> invokeResult(Object ctx) throws Exception {
|
||||||
|
return invokeMono("result", new Class<?>[] {contextClass()}, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GitRoute mockRoute(GitRouteOperation op, ArtifactType type) {
|
||||||
|
GitRoute route = Mockito.mock(GitRoute.class);
|
||||||
|
when(route.artifactType()).thenReturn(ArtifactType.APPLICATION);
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GitRoute mockRoute(GitRouteOperation op, ArtifactType type, String fieldName) {
|
||||||
|
GitRoute route = Mockito.mock(GitRoute.class);
|
||||||
|
when(route.operation()).thenReturn(op);
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Artifact newArtifact(String workspaceId, GitArtifactMetadata meta) {
|
||||||
|
Application a = new Application();
|
||||||
|
a.setWorkspaceId(workspaceId);
|
||||||
|
a.setGitArtifactMetadata(meta);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- getLocalBranches ----------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("getLocalBranches: returns only branch ref names, excluding tags")
|
||||||
|
void getLocalBranches_positive_filtersTags() throws Exception {
|
||||||
|
Object ctx = newContext();
|
||||||
|
|
||||||
|
// route uses APPLICATION to select corresponding helper
|
||||||
|
setCtx(ctx, "gitRoute", mockRoute(GitRouteOperation.STATUS, ArtifactType.APPLICATION));
|
||||||
|
|
||||||
|
// parent with base id
|
||||||
|
GitArtifactMetadata parentMeta = newMeta("base-123", "git@github.com:org/repo.git", "repo");
|
||||||
|
Application parent = new Application();
|
||||||
|
parent.setGitArtifactMetadata(parentMeta);
|
||||||
|
setCtx(ctx, "parent", parent);
|
||||||
|
|
||||||
|
// five applications: 3 branches, 2 tags
|
||||||
|
Application a1 = new Application();
|
||||||
|
GitArtifactMetadata m1 = new GitArtifactMetadata();
|
||||||
|
m1.setRefType(RefType.branch);
|
||||||
|
m1.setRefName("b1");
|
||||||
|
a1.setGitArtifactMetadata(m1);
|
||||||
|
|
||||||
|
Application a2 = new Application();
|
||||||
|
GitArtifactMetadata m2 = new GitArtifactMetadata();
|
||||||
|
m2.setRefType(RefType.tag);
|
||||||
|
m2.setRefName("t1");
|
||||||
|
a2.setGitArtifactMetadata(m2);
|
||||||
|
|
||||||
|
Application a3 = new Application();
|
||||||
|
GitArtifactMetadata m3 = new GitArtifactMetadata();
|
||||||
|
m3.setRefType(RefType.branch);
|
||||||
|
m3.setRefName("b2");
|
||||||
|
a3.setGitArtifactMetadata(m3);
|
||||||
|
|
||||||
|
Application a4 = new Application();
|
||||||
|
GitArtifactMetadata m4 = new GitArtifactMetadata();
|
||||||
|
m4.setRefType(RefType.tag);
|
||||||
|
m4.setRefName("t2");
|
||||||
|
a4.setGitArtifactMetadata(m4);
|
||||||
|
|
||||||
|
Application a5 = new Application();
|
||||||
|
GitArtifactMetadata m5 = new GitArtifactMetadata();
|
||||||
|
m5.setRefType(RefType.branch);
|
||||||
|
m5.setRefName("b3");
|
||||||
|
a5.setGitArtifactMetadata(m5);
|
||||||
|
|
||||||
|
// stubs
|
||||||
|
when(gitRouteArtifact.getArtifactHelper(ArtifactType.APPLICATION))
|
||||||
|
.thenReturn((GitArtifactHelper) gitArtifactHelper);
|
||||||
|
when(gitArtifactHelper.getAllArtifactByBaseId("base-123", null))
|
||||||
|
.thenAnswer(getArtifactAnswer(List.of(a1, a2, a3, a4, a5)));
|
||||||
|
|
||||||
|
StepVerifier.create(invokeGetLocalBranches(ctx))
|
||||||
|
.assertNext(result -> {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
java.util.List<String> branches = (java.util.List<String>) result;
|
||||||
|
assertThat(branches).hasSize(3);
|
||||||
|
assertThat(branches).containsExactly("b1", "b2", "b3");
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T extends Artifact> Answer<Flux<T>> getArtifactAnswer(List<T> appList) {
|
||||||
|
return invocationOnMock -> Flux.fromIterable(appList);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GitArtifactMetadata newMeta(String baseId, String remoteUrl, String repoName) {
|
||||||
|
GitArtifactMetadata m = new GitArtifactMetadata();
|
||||||
|
m.setDefaultArtifactId(baseId);
|
||||||
|
m.setRemoteUrl(remoteUrl);
|
||||||
|
m.setRepoName(repoName);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- routeFilter ----------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("routeFilter: emits TRUE when operation requires git FS ops")
|
||||||
|
void routeFilter_positive() throws Exception {
|
||||||
|
Object ctx = newContext();
|
||||||
|
setCtx(ctx, "gitRoute", mockRoute(GitRouteOperation.COMMIT, ArtifactType.APPLICATION, "id"));
|
||||||
|
|
||||||
|
StepVerifier.create(invokeRouteFilter(ctx))
|
||||||
|
.assertNext(nodeResult -> {
|
||||||
|
Boolean isFsOp = (Boolean) nodeResult;
|
||||||
|
assertThat(isFsOp).isTrue();
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("routeFilter: errors when operation does not require git FS ops")
|
||||||
|
void routeFilter_negative() throws Exception {
|
||||||
|
Object ctx = newContext();
|
||||||
|
setCtx(ctx, "gitRoute", mockRoute(GitRouteOperation.METADATA, ArtifactType.APPLICATION, "id"));
|
||||||
|
|
||||||
|
StepVerifier.create(invokeRouteFilter(ctx))
|
||||||
|
.expectErrorSatisfies(err -> {
|
||||||
|
assertThat(err).isInstanceOf(AppsmithException.class);
|
||||||
|
assertThat(((AppsmithException) err).getAppErrorCode())
|
||||||
|
.isEqualTo(AppsmithError.GIT_ROUTE_FS_OPS_NOT_REQUIRED.getAppErrorCode());
|
||||||
|
})
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- clean-up-filter ----------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("cleanUpFilter: emits TRUE when operation does not require cleanup")
|
||||||
|
void cleanUpFilter_positive() throws Exception {
|
||||||
|
Object ctx = newContext();
|
||||||
|
setCtx(ctx, "gitRoute", mockRoute(GitRouteOperation.COMMIT, ArtifactType.APPLICATION, "id"));
|
||||||
|
|
||||||
|
StepVerifier.create(invokeCleanUpFilter(ctx))
|
||||||
|
.assertNext(nodeResult -> {
|
||||||
|
Boolean notRequiresFilter = (Boolean) nodeResult;
|
||||||
|
assertThat(notRequiresFilter).isTrue();
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("cleanUpFilter: errors when operation require cleanup")
|
||||||
|
void cleanUp_negative() throws Exception {
|
||||||
|
Object ctx = newContext();
|
||||||
|
setCtx(ctx, "gitRoute", mockRoute(GitRouteOperation.DISCONNECT, ArtifactType.APPLICATION, "id"));
|
||||||
|
|
||||||
|
StepVerifier.create(invokeCleanUpFilter(ctx))
|
||||||
|
.expectErrorSatisfies(err -> {
|
||||||
|
assertThat(err).isInstanceOf(AppsmithException.class);
|
||||||
|
assertThat(((AppsmithException) err).getAppErrorCode())
|
||||||
|
.isEqualTo(AppsmithError.GIT_ROUTE_FS_CLEAN_UP_REQUIRED.getAppErrorCode());
|
||||||
|
})
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- metadataFilter ----------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("metadataFilter: emits TRUE when metadata is complete")
|
||||||
|
void metadataFilter_positive() throws Exception {
|
||||||
|
Object ctx = newContext();
|
||||||
|
GitArtifactMetadata meta = newMeta("baseId", "git@github.com:repo.git", "repo");
|
||||||
|
setCtx(ctx, "artifact", newArtifact("ws1", meta));
|
||||||
|
|
||||||
|
StepVerifier.create(invokeMetadataFilter(ctx))
|
||||||
|
.assertNext(result -> {
|
||||||
|
assertThat(result).isEqualTo(Boolean.TRUE);
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("metadataFilter: errors when metadata is missing/blank")
|
||||||
|
void metadataFilter_negative() throws Exception {
|
||||||
|
Object ctx = newContext();
|
||||||
|
GitArtifactMetadata meta = newMeta(null, null, null);
|
||||||
|
setCtx(ctx, "artifact", newArtifact("ws1", meta));
|
||||||
|
|
||||||
|
StepVerifier.create(invokeMetadataFilter(ctx))
|
||||||
|
.expectErrorSatisfies(err -> {
|
||||||
|
assertThat(err).isInstanceOf(AppsmithException.class);
|
||||||
|
assertThat(((AppsmithException) err).getAppErrorCode())
|
||||||
|
.isEqualTo(AppsmithError.GIT_ROUTE_METADATA_NOT_FOUND.getAppErrorCode());
|
||||||
|
})
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- gitProfile ----------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("gitProfile: emits profile when present")
|
||||||
|
void gitProfile_positive() throws Exception {
|
||||||
|
Object ctx = newContext();
|
||||||
|
setCtx(ctx, "fieldValue", "user-id");
|
||||||
|
GitProfile profile = new GitProfile();
|
||||||
|
profile.setAuthorEmail("u@example.com");
|
||||||
|
profile.setAuthorName("User");
|
||||||
|
when(gitProfileUtils.getGitProfileForUser("user-id")).thenReturn(Mono.just(profile));
|
||||||
|
|
||||||
|
StepVerifier.create(invokeGitProfile(ctx))
|
||||||
|
.assertNext(gitProfile -> {
|
||||||
|
GitProfile testProfile = (GitProfile) gitProfile;
|
||||||
|
assertThat(testProfile).isNotNull();
|
||||||
|
assertThat(testProfile.getAuthorName()).isEqualTo("User");
|
||||||
|
assertThat(testProfile.getAuthorEmail()).isEqualTo("u@example.com");
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("gitProfile: errors when profile not configured")
|
||||||
|
void gitProfile_negative() throws Exception {
|
||||||
|
Object ctx = newContext();
|
||||||
|
setCtx(ctx, "fieldValue", "user-id");
|
||||||
|
when(gitProfileUtils.getGitProfileForUser("user-id")).thenReturn(Mono.empty());
|
||||||
|
StepVerifier.create(invokeGitProfile(ctx))
|
||||||
|
.expectErrorSatisfies(err -> {
|
||||||
|
assertThat(err).isInstanceOf(AppsmithException.class);
|
||||||
|
assertThat(((AppsmithException) err).getAppErrorCode())
|
||||||
|
.isEqualTo(AppsmithError.INVALID_GIT_CONFIGURATION.getAppErrorCode());
|
||||||
|
})
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- gitAuth ----------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("gitAuth: emits auth when present")
|
||||||
|
void gitAuth_positive() throws Exception {
|
||||||
|
Object ctx = newContext();
|
||||||
|
GitArtifactMetadata meta = newMeta("baseId", "git@github.com:repo.git", "repo");
|
||||||
|
GitAuth auth = new GitAuth();
|
||||||
|
auth.setPrivateKey(
|
||||||
|
"-----BEGIN OPENSSH PRIVATE KEY-----\nAAAAB3NzaC1yc2EAAAADAQABAAABAQ==\n-----END OPENSSH PRIVATE KEY-----\n");
|
||||||
|
auth.setPublicKey("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ test");
|
||||||
|
meta.setGitAuth(auth);
|
||||||
|
setCtx(ctx, "gitMeta", meta);
|
||||||
|
|
||||||
|
StepVerifier.create(invokeGitAuth(ctx))
|
||||||
|
.assertNext(gitAuth -> {
|
||||||
|
GitAuth gitAuth1 = (GitAuth) gitAuth;
|
||||||
|
assertThat(gitAuth1).isNotNull();
|
||||||
|
assertThat(gitAuth1.getPublicKey()).isEqualTo("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ test");
|
||||||
|
assertThat(gitAuth1.getPrivateKey())
|
||||||
|
.isEqualTo(
|
||||||
|
"-----BEGIN OPENSSH PRIVATE KEY-----\nAAAAB3NzaC1yc2EAAAADAQABAAABAQ==\n-----END OPENSSH PRIVATE KEY-----\n");
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("gitAuth: errors when missing")
|
||||||
|
void gitAuth_negative() throws Exception {
|
||||||
|
Object ctx = newContext();
|
||||||
|
GitArtifactMetadata meta = newMeta("baseId", "git@github.com:repo.git", "repo");
|
||||||
|
meta.setGitAuth(null);
|
||||||
|
setCtx(ctx, "gitMeta", meta);
|
||||||
|
|
||||||
|
StepVerifier.create(invokeGitAuth(ctx))
|
||||||
|
.expectErrorSatisfies(err -> {
|
||||||
|
assertThat(err).isInstanceOf(AppsmithException.class);
|
||||||
|
assertThat(((AppsmithException) err).getAppErrorCode())
|
||||||
|
.isEqualTo(AppsmithError.INVALID_GIT_CONFIGURATION.getAppErrorCode());
|
||||||
|
})
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- keys (repoKey/lockKey) ----------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("repoKey: emits formatted repo key from context")
|
||||||
|
void repoKey_positive() throws Exception {
|
||||||
|
Object ctx = newContext();
|
||||||
|
GitArtifactMetadata meta = newMeta("base-app", "git@github.com:org/repo.git", "repo");
|
||||||
|
Artifact artifact = newArtifact("ws-123", meta);
|
||||||
|
setCtx(ctx, "artifact", artifact);
|
||||||
|
setCtx(ctx, "gitMeta", meta);
|
||||||
|
|
||||||
|
StepVerifier.create(invokeRepoKey(ctx))
|
||||||
|
.assertNext(key -> {
|
||||||
|
assertThat(key).isInstanceOf(String.class);
|
||||||
|
assertThat((String) key)
|
||||||
|
.isEqualTo("purpose=repo/v=1/workspace=ws-123/artifact=base-app/repository=repo/");
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("lockKey: formats lock key even when repoKey is null")
|
||||||
|
void lockKey_nullRepoKey_yieldsString() throws Exception {
|
||||||
|
Object ctx = newContext();
|
||||||
|
setCtx(ctx, "repoKey", null);
|
||||||
|
|
||||||
|
StepVerifier.create(invokeLockKey(ctx))
|
||||||
|
.assertNext(key -> assertThat((String) key).isEqualTo("purpose=lock/null"))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user