/
psklyarov
/
agent-replacer
Обзор
Документация
Войти
/
psklyarov
/
agent-replacer
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/main/java/com/example/agentreplacer/bitbucket/BitbucketService.java
363 строки
18 KB
pavelsklyarov
insecure ssl
16 дек 2025, 09:03
16 дек 2025, 09:03
da61b1f
Код
Авторство
О чём код?
package com.example.agentreplacer.bitbucket; import com.example.agentreplacer.plan.ChangePlan; import com.example.agentreplacer.processor.FileProcessorService; import com.example.agentreplacer.chat.ChatService; import lombok.RequiredArgsConstructor; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; import org.springframework.http.client.JdkClientHttpRequestFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URI; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.net.http.HttpClient; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Pattern; @Service @RequiredArgsConstructor public class BitbucketService { private final BitbucketProperties properties; private final FileProcessorService fileProcessorService; private final ChatService chatService; private RestClient bitbucketClient; private static final Logger log = LoggerFactory.getLogger(BitbucketService.class); public List<Map<String, Object>> listRepositories(String projectKey) { ensureClient(); if (projectKey == null || projectKey.isBlank()) { throw new IllegalArgumentException("projectKey обязателен и должен быть задан в плане"); } log.info("Listing repositories for project {}", projectKey); List<Map<String, Object>> all = new ArrayList<>(); Integer start = null; boolean isLastPage = false; while (!isLastPage) { String uri = start == null ? "/rest/api/1.0/projects/{projectKey}/repos?limit=100" : "/rest/api/1.0/projects/{projectKey}/repos?limit=100&start=" + start; Map response = bitbucketClient.get() .uri(uri, projectKey) .retrieve() .body(Map.class); if (response == null) break; Object values = response.get("values"); if (values instanceof List<?> list) { log.debug("Fetched {} repos page chunk", list.size()); for (Object o : list) { if (o instanceof Map<?, ?> m) { all.add((Map<String, Object>) m); } } } Object lastPage = response.get("isLastPage"); isLastPage = lastPage instanceof Boolean b && b; if (!isLastPage) { Object next = response.get("nextPageStart"); start = (next instanceof Number n) ? n.intValue() : null; if (start == null) break; } } log.info("Total repositories found: {}", all.size()); return all; } public void processAllRepositories(ChangePlan plan, String sessionId) { Pattern includeRepo = safePattern(plan.getIncludeRepoRegex()); Pattern excludeRepo = safePattern(plan.getExcludeRepoRegex()); List<Map<String, Object>> repos = listRepositories(plan.getProjectKey()); for (Map<String, Object> repo : repos) { String slug = String.valueOf(repo.getOrDefault("slug", "")); if (excludeRepo != null && excludeRepo.matcher(slug).find()) { log.debug("Skip repo by exclude pattern: {}", slug); if (sessionId != null) { chatService.addAssistantMessage(sessionId, "Пропускаю репозиторий: " + slug + " (совпадает с exclude)"); } continue; } if (includeRepo != null && !includeRepo.matcher(slug).find()) { log.debug("Skip repo not matching include pattern: {}", slug); if (sessionId != null) { chatService.addAssistantMessage(sessionId, "Пропускаю репозиторий: " + slug + " (не подходит под include)"); } continue; } try { if (sessionId != null) { chatService.addAssistantMessage(sessionId, "Репозиторий: " + slug); } log.info("Processing repository {}", slug); processSingleRepository(slug, plan, sessionId); } catch (Exception e) { // логируем и идём дальше log.error("Error processing repo {}: {}", slug, e.getMessage(), e); if (sessionId != null) { chatService.addAssistantMessage(sessionId, "Ошибка при обработке репозитория " + slug + ": " + e.getMessage()); } } } } private void processSingleRepository(String repoSlug, ChangePlan plan, String sessionId) throws IOException, GitAPIException { Path workspaceRoot = Path.of(properties.getWorkspaceDir() == null ? "./workspace" : properties.getWorkspaceDir()); Path sessionRoot = workspaceRoot.resolve(sessionId == null || sessionId.isBlank() ? "default" : sessionId); Files.createDirectories(sessionRoot); Path repoDir = sessionRoot.resolve(repoSlug); String cloneUrl = buildCloneUrl(plan.getProjectKey(), repoSlug); // Очистка каталога, если там уже есть данные прошлых запусков if (Files.exists(repoDir)) { if (sessionId != null) { chatService.addAssistantMessage(sessionId, "Очищаю каталог репозитория: " + repoSlug); } deleteDirectory(repoDir); if (sessionId != null) { chatService.addAssistantMessage(sessionId, "Каталог очищен: " + repoSlug); } } try (Git ignored = Git.cloneRepository() .setURI(cloneUrl) .setDirectory(repoDir.toFile()) .setCredentialsProvider(creds()) .call()) { // ok log.info("Cloned repo {} into {}", repoSlug, repoDir); if (sessionId != null) { chatService.addAssistantMessage(sessionId, "Клонирован репозиторий: " + repoSlug); } } List<String> bases = plan.getTargetBranches() != null && !plan.getTargetBranches().isEmpty() ? plan.getTargetBranches() : List.of("main"); for (String baseBranch : bases) { switch (plan.getMode() == null ? ChangePlan.Mode.FEATURE_PR : plan.getMode()) { case FEATURE_PR -> { log.info("Mode FEATURE_PR: base={} repo={}", baseBranch, repoSlug); String featureBase = sanitizeBranch(baseBranch); String featureNamePrefix = plan.getBranchName() == null ? "feature/" + plan.getTaskNumber() : plan.getBranchName(); String featureBranch = featureNamePrefix + "-" + featureBase; try (Git git = Git.open(repoDir.toFile())) { // checkout базовой ветки (если нет - пробуем master) if (sessionId != null) chatService.addAssistantMessage(sessionId, "Переключаюсь на базовую ветку: " + baseBranch + " (" + repoSlug + ")"); try { git.checkout().setName(baseBranch).call(); } catch (Exception e) { log.warn("Base branch {} not found, fallback to master in {}", baseBranch, repoSlug); git.checkout().setName("master").call(); if (sessionId != null) chatService.addAssistantMessage(sessionId, "Ветка " + baseBranch + " не найдена, использую master (" + repoSlug + ")"); } if (sessionId != null) chatService.addAssistantMessage(sessionId, "Обновляю ветку: pull (" + repoSlug + ":" + baseBranch + ")"); git.pull().setCredentialsProvider(creds()).call(); git.checkout().setCreateBranch(true).setName(featureBranch).call(); } if (sessionId != null) { chatService.addAssistantMessage(sessionId, "Создана ветка: " + featureBranch + " от " + baseBranch + " (" + repoSlug + ")"); chatService.addAssistantMessage(sessionId, "Обрабатываю файлы (" + repoSlug + ":" + featureBranch + ") …"); } int changed = fileProcessorService.processRepositoryFiles(repoDir, plan, sessionId, repoSlug, featureBranch); if (changed == 0) { if (sessionId != null) chatService.addAssistantMessage(sessionId, "Изменений нет для ветки " + baseBranch + " (" + repoSlug + ")"); continue; } try (Git git = Git.open(repoDir.toFile())) { if (sessionId != null) chatService.addAssistantMessage(sessionId, "Готовлю коммит (" + changed + " файлов) в " + featureBranch + " (" + repoSlug + ")"); git.add().addFilepattern(".").call(); String commitMsg = plan.getCommitMessage() != null ? plan.getCommitMessage() : plan.getTaskNumber() + ": bulk changes"; git.commit().setMessage(commitMsg).call(); if (sessionId != null) chatService.addAssistantMessage(sessionId, "Пушу ветку " + featureBranch + " (" + repoSlug + ")"); git.push().setCredentialsProvider(creds()).call(); if (sessionId != null) { chatService.addAssistantMessage(sessionId, "Коммит выполнен: " + repoSlug + " → " + featureBranch + " (" + changed + " файлов)"); chatService.addAssistantMessage(sessionId, "Создаю Pull Request в " + baseBranch + " (" + repoSlug + ")"); } } createPullRequest(plan.getProjectKey(), repoSlug, featureBranch, baseBranch, plan.getTaskNumber(), plan.getCommitMessage(), sessionId); } case DIRECT_COMMIT -> { log.info("Mode DIRECT_COMMIT: base={} repo={}", baseBranch, repoSlug); try (Git git = Git.open(repoDir.toFile())) { if (sessionId != null) chatService.addAssistantMessage(sessionId, "Переключаюсь на ветку: " + baseBranch + " (" + repoSlug + ")"); try { git.checkout().setName(baseBranch).call(); } catch (Exception e) { log.warn("Base branch {} not found, fallback to master in {}", baseBranch, repoSlug); git.checkout().setName("master").call(); if (sessionId != null) chatService.addAssistantMessage(sessionId, "Ветка " + baseBranch + " не найдена, использую master (" + repoSlug + ")"); } if (sessionId != null) chatService.addAssistantMessage(sessionId, "Обновляю ветку: pull (" + repoSlug + ":" + baseBranch + ")"); git.pull().setCredentialsProvider(creds()).call(); } if (sessionId != null) chatService.addAssistantMessage(sessionId, "Коммичу напрямую в ветку: " + baseBranch + " (" + repoSlug + ")"); int changed = fileProcessorService.processRepositoryFiles(repoDir, plan, sessionId, repoSlug, baseBranch); if (changed == 0) { if (sessionId != null) chatService.addAssistantMessage(sessionId, "Изменений нет для ветки " + baseBranch + " (" + repoSlug + ")"); continue; } try (Git git = Git.open(repoDir.toFile())) { if (sessionId != null) chatService.addAssistantMessage(sessionId, "Готовлю коммит (" + changed + " файлов) в " + baseBranch + " (" + repoSlug + ")"); git.add().addFilepattern(".").call(); String commitMsg = plan.getCommitMessage() != null ? plan.getCommitMessage() : plan.getTaskNumber() + ": bulk changes"; git.commit().setMessage(commitMsg).call(); if (sessionId != null) chatService.addAssistantMessage(sessionId, "Пушу ветку " + baseBranch + " (" + repoSlug + ")"); git.push().setCredentialsProvider(creds()).call(); if (sessionId != null) { chatService.addAssistantMessage(sessionId, "Коммит выполнен в " + baseBranch + " (" + repoSlug + ", " + changed + " файлов)"); } } } case CONFIRM -> { log.info("Mode CONFIRM: base={} repo={}", baseBranch, repoSlug); try (Git git = Git.open(repoDir.toFile())) { if (sessionId != null) chatService.addAssistantMessage(sessionId, "Переключаюсь на ветку: " + baseBranch + " (" + repoSlug + ")"); try { git.checkout().setName(baseBranch).call(); } catch (Exception e) { log.warn("Base branch {} not found, fallback to master in {}", baseBranch, repoSlug); git.checkout().setName("master").call(); if (sessionId != null) chatService.addAssistantMessage(sessionId, "Ветка " + baseBranch + " не найдена, использую master (" + repoSlug + ")"); } if (sessionId != null) chatService.addAssistantMessage(sessionId, "Обновляю ветку: pull (" + repoSlug + ":" + baseBranch + ")"); git.pull().setCredentialsProvider(creds()).call(); } int changed = fileProcessorService.processRepositoryFiles(repoDir, plan, sessionId, repoSlug, baseBranch); if (sessionId != null) { chatService.addAssistantMessage(sessionId, "Требуется подтверждение: " + repoSlug + " / " + baseBranch + " — изменено файлов: " + changed + ". " + "Подтвердить: POST /api/run/" + sessionId + "/confirm {\"repoSlug\":\"" + repoSlug + "\",\"baseBranch\":\"" + baseBranch + "\",\"decision\":\"approve|reject\"}"); } log.info("Awaiting confirmation for repo={} base={} changedFiles={}", repoSlug, baseBranch, changed); // Коммит будет произведён по confirm API } } } } private UsernamePasswordCredentialsProvider creds() { return new UsernamePasswordCredentialsProvider( properties.getUsername(), properties.getPassword() ); } private String buildCloneUrl(String projectKey, String repoSlug) { // HTTP(S) clone: {baseUrl}/scm/{projectKey}/{slug}.git String base = properties.getBaseUrl(); if (projectKey == null || projectKey.isBlank()) { throw new IllegalArgumentException("projectKey обязателен и должен быть задан в плане"); } String encodedSlug = URLEncoder.encode(repoSlug, StandardCharsets.UTF_8); return URI.create(base + "/scm/" + projectKey + "/" + encodedSlug + ".git").toString(); } private Pattern safePattern(String regex) { if (regex == null || regex.isBlank()) return null; return Pattern.compile(regex); } private void ensureClient() { if (bitbucketClient == null) { RestClient.Builder b = RestClient.builder() .baseUrl(properties.getBaseUrl()) .defaultHeaders(headers -> headers.setBasicAuth(properties.getUsername(), properties.getPassword())); if (properties.isInsecureSsl()) { try { b = b.requestFactory(insecureRequestFactory()); log.warn("Bitbucket REST client configured with insecure SSL (cert/host checks disabled)"); } catch (Exception e) { log.warn("Failed to set insecure SSL for Bitbucket client: {}", e.getMessage()); } } bitbucketClient = b.build(); log.debug("Bitbucket REST client initialized for {}", properties.getBaseUrl()); } } private JdkClientHttpRequestFactory insecureRequestFactory() throws Exception { TrustManager[] trustAll = new TrustManager[] { new X509TrustManager() { public java.security.cert.X509Certificate[] getAcceptedIssuers() { return new java.security.cert.X509Certificate[0]; } public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {} public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {} } }; SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustAll, new java.security.SecureRandom()); SSLParameters params = new SSLParameters(); // отключаем проверку имени хоста params.setEndpointIdentificationAlgorithm(null); HttpClient client = HttpClient.newBuilder() .sslContext(sslContext) .sslParameters(params) .build(); return new JdkClientHttpRequestFactory(client); } private String sanitizeBranch(String name) { if (name == null) return "base"; return name.replaceAll("[^A-Za-z0-9._\\-/]", "-"); } private void createPullRequest(String projectKey, String repoSlug, String fromBranch, String toBranch, String taskNumber, String commitMessage, String sessionId) { ensureClient(); String title = (taskNumber != null ? taskNumber + ": " : "") + "Автогенерированный PR"; String description = (commitMessage != null ? commitMessage : "Bulk changes"); Map<String, Object> body = Map.of( "title", title, "description", description, "state", "OPEN", "open", true, "closed", false, "fromRef", Map.of("id", "refs/heads/" + fromBranch), "toRef", Map.of("id", "refs/heads/" + toBranch) ); try { bitbucketClient.post() .uri("/rest/api/1.0/projects/{projectKey}/repos/{repo}/pull-requests", projectKey, repoSlug) .body(body) .retrieve() .toBodilessEntity(); log.info("PR created: repo={} from={} to={}", repoSlug, fromBranch, toBranch); if (sessionId != null) { chatService.addAssistantMessage(sessionId, "PR создан: " + repoSlug + " " + fromBranch + " → " + toBranch); } } catch (Exception ignored) { // логируем при необходимости log.warn("Failed to create PR: repo={} from={} to={}", repoSlug, fromBranch, toBranch); if (sessionId != null) { chatService.addAssistantMessage(sessionId, "Не удалось создать PR: " + repoSlug + " " + fromBranch + " → " + toBranch); } } } private void deleteDirectory(Path dir) throws IOException { if (!Files.exists(dir)) return; try (var walk = Files.walk(dir)) { walk.sorted(java.util.Comparator.reverseOrder()) .forEach(path -> { try { Files.deleteIfExists(path); } catch (IOException e) { throw new RuntimeException("Не удалось удалить " + path + ": " + e.getMessage(), e); } }); } } }