diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 0000000..83db9e6 --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,48 @@ +name: Build Release + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: ./gradlew build -x test + + - name: Create Release + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ github.run_number }} + release_name: Release v${{ github.run_number }} + draft: false + prerelease: false + + - name: Upload JAR Artifact + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: build/libs/*.jar + asset_name: commitet-jm.jar + asset_content_type: application/java-archive \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..f237c7b --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,82 @@ +# Архитектура проекта Commitet JM + +## Текущая архитектура + +```mermaid +graph TD + A[CommitetJmApplication] --> B[QuartzConfig] + A --> C[SecurityConfig] + A --> D[Services] + A --> E[Components] + + B --> F[Committer Job] + F --> G[GitWorker] + + D --> G + D --> H[ChatHistoryService] + D --> I[OneRunner] + + E --> J[ShellExecutor] + + G --> J + G --> K[DataManager] + G --> L[FileStorageLocator] + G --> I + + H --> K + + I --> J + I --> K + + subgraph Entities + M[Project] + N[Commit] + O[FileCommit] + P[User] + Q[Platform] + R[OneCStorage] + S[ChatSession] + T[ChatMessage] + end + + G --> M + G --> N + G --> O + G --> P + G --> Q + R --> M + N --> O + N --> P + S --> P + T --> S +``` + +## Основные компоненты + +### 1. Основное приложение +- `CommitetJmApplication` - главный класс Spring Boot приложения + +### 2. Конфигурация +- `QuartzConfig` - настройка задач планировщика +- Security конфигурация + +### 3. Сервисы +- `GitWorker` - основной сервис для работы с Git и файлами +- `ChatHistoryService` - сервис для работы с чатом +- `OneRunner` - сервис для работы с OneC + +### 4. Компоненты +- `ShellExecutor` - компонент для выполнения shell команд + +### 5. Scheduled Jobs +- `Committer` - задача для автоматического создания коммитов + +### 6. Сущности (Entities) +- `Project` - проект +- `Commit` - коммит +- `FileCommit` - файл коммита +- `User` - пользователь +- `Platform` - платформа OneC +- `OneCStorage` - хранилище OneC +- `ChatSession` - сессия чата +- `ChatMessage` - сообщение чата \ No newline at end of file diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md new file mode 100644 index 0000000..b58211a --- /dev/null +++ b/IMPROVEMENTS.md @@ -0,0 +1,321 @@ +# Рекомендации по улучшению архитектуры проекта + +## 1. Улучшения структуры проекта + +### 1.1. Переход к многоуровневой архитектуре + +Текущая структура проекта не имеет четкого разделения на слои. Рекомендуется перейти к многоуровневой архитектуре: + +``` +src/main/kotlin/com/company/commitet_jm/ +├── application/ # Слой приложения (use cases) +├── domain/ # Доменный слой (entities, repositories, services) +├── infrastructure/ # Инфраструктурный слой (конфигурации, внешние сервисы) +├── presentation/ # Слой представления (view, контроллеры) +└── shared/ # Общие компоненты +``` + +### 1.2. Улучшение структуры пакетов + +#### Текущая структура: +- service/ - содержит все сервисы +- component/ - содержит вспомогательные компоненты +- entity/ - содержит все сущности + +#### Рекомендуемая структура: +``` +domain/ +├── project/ # Все, что связано с проектами +│ ├── entity/ +│ ├── repository/ +│ └── service/ +├── commit/ # Все, что связано с коммитами +│ ├── entity/ +│ ├── repository/ +│ └── service/ +├── user/ # Все, что связано с пользователями +│ ├── entity/ +│ ├── repository/ +│ └── service/ +├── chat/ # Все, что связано с чатом +│ ├── entity/ +│ ├── repository/ +│ └── service/ +└── ones/ # Все, что связано с OneC + ├── entity/ + ├── repository/ + └── service/ + +infrastructure/ +├── git/ # Инфраструктурные компоненты для работы с Git +├── shell/ # Компоненты для работы с shell командами +├── quartz/ # Конфигурация Quartz +└── persistence/ # Конфигурация базы данных + +presentation/ +├── view/ # Vaadin views +└── rest/ # REST API (если планируется) + +shared/ +├── exception/ # Общие исключения +├── util/ # Утилитарные классы +└── config/ # Общая конфигурация +``` + +## 2. Улучшения читаемости и поддерживаемости кода + +### 2.1. Разделение ответственностей + +#### Проблема: +Класс `GitWorker` имеет слишком много ответственностей: +- Работа с Git репозиториями +- Работа с файлами +- Работа с OneC +- Обработка ошибок +- Управление ветками + +#### Решение: +Разделить на несколько специализированных сервисов: + +```kotlin +// Сервис для работы с Git +interface GitService { + fun cloneRepo(url: String, directoryPath: String, branch: String): Result + fun createBranch(repoPath: String, branchName: String): Result + fun checkoutBranch(repoPath: String, branchName: String): Result + fun commitChanges(repoPath: String, message: String, author: GitAuthor): Result + fun pushChanges(repoPath: String, branchName: String): Result +} + +// Сервис для работы с файлами +interface FileService { + fun saveFile(baseDir: String, fileName: String, content: ByteArray, type: FileType): Result + fun createTempFile(content: String): Result + fun deleteTempFile(file: File): Result +} + +// Сервис для работы с OneC +interface OneCService { + fun uploadExternalFiles(inputFile: File, outDir: String, platformPath: String, version: String): Result + fun unpackExternalFiles(inputFile: File, outDir: String): Result +} +``` + +### 2.2. Улучшение обработки ошибок + +#### Проблема: +Текущая обработка ошибок использует исключения без четкой структуры. + +#### Решение: +Ввести типизированную обработку ошибок с использованием sealed классов: + +```kotlin +sealed class GitError { + object RepositoryNotFound : GitError() + object InvalidUrl : GitError() + object BranchExists : GitError() + data class CommandFailed(val message: String) : GitError() +} + +sealed class FileError { + object DirectoryNotEmpty : FileError() + data class IoError(val message: String) : FileError() +} + +sealed class OneCError { + data class PlatformNotFound(val path: String) : OneCError() + data class UnpackFailed(val message: String) : OneCError() +} + +typealias GitResult = Result +typealias FileResult = Result +typealias OneCResult = Result +``` + +### 2.3. Улучшение конфигурации + +#### Проблема: +Жестко заданные пути в коде (`DataProcessorsExt\\erf`, `DataProcessorsExt\\epf`). + +#### Решение: +Вынести конфигурацию в application.properties/application.yml: + +```yaml +app: + paths: + data-processors: + erf: DataProcessorsExt/erf + epf: DataProcessorsExt/epf + code-ext: CodeExt + exchange-rules: EXCHANGE_RULES +``` + +И создать конфигурационный класс: + +```kotlin +@ConfigurationProperties(prefix = "app.paths") +data class AppPathsConfig( + val dataProcessors: DataProcessorsPaths, + val codeExt: String, + val exchangeRules: String +) { + data class DataProcessorsPaths( + val erf: String, + val epf: String + ) +} +``` + +### 2.4. Улучшение логирования + +#### Проблема: +В `ShellExecutor` используется логгер от `GitWorker`. + +#### Решение: +Каждый класс должен использовать свой собственный логгер: + +```kotlin +@Component +class ShellExecutor(var workingDir: File = File("."), var timeout: Long = 1) { + companion object { + private val log = LoggerFactory.getLogger(ShellExecutor::class.java) + } + // ... +} +``` + +## 3. Улучшения архитектуры + +### 3.1. Внедрение зависимостей + +#### Проблема: +В `Committer` создается новый экземпляр `GitWorker` напрямую. + +#### Решение: +Использовать внедрение зависимостей Spring: + +```kotlin +@Component +class Committer( + private val gitWorker: GitWorker +): Job { + // ... +} +``` + +### 3.2. Улучшение работы с файлами + +#### Проблема: +Работа с путями файлов через жестко заданные строки. + +#### Решение: +Использовать Path API для работы с путями: + +```kotlin +private fun correctPath(baseDir: Path, type: TypesFiles): Path { + return when (type) { + REPORT -> baseDir.resolve("DataProcessorsExt").resolve("Отчет") + DATAPROCESSOR -> baseDir.resolve("DataProcessorsExt").resolve("Обработка") + SCHEDULEDJOBS -> baseDir.resolve("CodeExt") + EXTERNAL_CODE -> baseDir.resolve("CodeExt") + EXCHANGE_RULES -> baseDir.resolve("EXCHANGE_RULES") + } +} +``` + +### 3.3. Улучшение работы с временными файлами + +#### Проблема: +Создание и удаление временных файлов вручную. + +#### Решение: +Использовать try-with-resources аналог для автоматического управления временными файлами: + +```kotlin +fun withTempFile(content: String, action: (File) -> T): T { + val tempFile = File.createTempFile("temp", ".txt") + return try { + tempFile.writeText(content, Charsets.UTF_8) + action(tempFile) + } finally { + tempFile.delete() + } +} +``` + +## 4. Тестирование + +### 4.1. Введение unit-тестов + +Для каждого сервиса необходимо создать unit-тесты, используя mocking фреймворк (MockK): + +```kotlin +class GitWorkerTest { + private val dataManager = mockk() + private val fileStorageLocator = mockk() + private val shellExecutor = mockk() + private val oneRunner = mockk() + + private lateinit var gitWorker: GitWorker + + @BeforeEach + fun setUp() { + gitWorker = GitWorker(dataManager, fileStorageLocator, shellExecutor, oneRunner) + } + + @Test + fun `should clone repository successfully`() { + // Тестовая реализация + } +} +``` + +### 4.2. Введение интеграционных тестов + +Для проверки взаимодействия компонентов необходимо создать интеграционные тесты: + +```kotlin +@SpringBootTest +@Testcontainers +class GitWorkerIntegrationTest { + // Интеграционная тестовая реализация +} +``` + +## 5. Документация + +### 5.1. Введение документации по API + +Создать документацию по основным сервисам и их методам. + +### 5.2. Введение документации по архитектуре + +Расширить файл ARCHITECTURE.md с описанием новой архитектуры. + +## 6. CI/CD улучшения + +### 6.1. Добавление тестирования в GitHub Actions + +Расширить существующий workflow для запуска тестов: + +```yaml +- name: Run tests + run: ./gradlew test + +- name: Publish test results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + files: build/test-results/**/*.xml +``` + +### 6.2. Добавление статического анализа кода + +Включить проверки качества кода: + +```yaml +- name: Run detekt + run: ./gradlew detekt + +- name: Run ktlint + run: ./gradlew ktlintCheck \ No newline at end of file diff --git a/RECOMMENDATIONS.md b/RECOMMENDATIONS.md new file mode 100644 index 0000000..8ded77c --- /dev/null +++ b/RECOMMENDATIONS.md @@ -0,0 +1,261 @@ +# Рекомендации по улучшению архитектуры проекта Commitet JM + +## Введение + +Проект Commitet JM представляет собой приложение для автоматизации работы с Git репозиториями с поддержкой OneC. На основе анализа текущей архитектуры были выявлены ключевые области для улучшения, которые повысят читаемость, поддерживаемость и расширяемость кода. + +## Основные проблемы текущей архитектуры + +### 1. Нарушение принципов SOLID + +#### Принцип единственной ответственности (SRP) +Класс `GitWorker` нарушает этот принцип, так как отвечает за: +- Работу с Git репозиториями +- Работу с файловой системой +- Интеграцию с OneC +- Обработку ошибок +- Управление ветками Git + +#### Принцип открытости/закрытости (OCP) +Текущая архитектура затрудняет расширение функциональности без изменения существующего кода, особенно в классе `GitWorker`. + +#### Принцип подстановки Барбары Лисков (LSP) +Отсутствуют четкие интерфейсы и контракты, что затрудняет замену реализаций. + +#### Принцип разделения интерфейса (ISP) +Интерфейсы отсутствуют или не разделены должным образом. + +#### Принцип инверсии зависимостей (DIP) +Зависимости создаются напрямую, а не внедряются через конструктор или сеттер. + +### 2. Проблемы с читаемостью и поддерживаемостью + +#### Длинные методы +Методы `beforeCmdCommit` и `afterCmdCommit` содержат сложную логику и превышают рекомендуемую длину. + +#### Жестко заданные константы +Пути к директориям жестко заданы в коде, что затрудняет конфигурирование и развертывание. + +#### Непоследовательное логирование +В `ShellExecutor` используется логгер от другого класса. + +#### Отсутствие типизированной обработки ошибок +Используются исключения без четкой структуры и классификации. + +### 3. Архитектурные проблемы + +#### Отсутствие четкого разделения на слои +Нет явного разделения на presentation, business, data и infrastructure слои. + +#### Тесная связность компонентов +Компоненты слишком тесно связаны друг с другом, что затрудняет тестирование и модификацию. + +#### Отсутствие unit-тестов +Нет автоматизированных тестов для проверки бизнес-логики. + +## Рекомендуемые улучшения + +### 1. Переход к многоуровневой архитектуре + +Рекомендуется разделить приложение на следующие слои: + +#### Domain Layer (Доменный слой) +Содержит бизнес-логику и доменные сущности: +- Entities (Project, Commit, User, и т.д.) +- Repositories (интерфейсы для работы с данными) +- Domain Services (бизнес-логика, не зависящая от фреймворков) + +#### Application Layer (Слой приложения) +Содержит use cases и координирует работу доменных объектов: +- Use Cases / Commands / Queries +- Application Services + +#### Infrastructure Layer (Инфраструктурный слой) +Содержит реализации инфраструктурных компонентов: +- Реализации Repository интерфейсов +- Интеграции с внешними системами (Git, OneC) +- Конфигурации фреймворков + +#### Presentation Layer (Слой представления) +Содержит компоненты пользовательского интерфейса: +- Vaadin Views +- REST Controllers (если планируется API) + +### 2. Улучшение структуры пакетов + +Перейти от плоской структуры к функциональной: + +``` +com.company.commitet_jm +├── domain +│ ├── project +│ │ ├── entity +│ │ ├── repository +│ │ └── service +│ ├── commit +│ │ ├── entity +│ │ ├── repository +│ │ └── service +│ └── user +│ ├── entity +│ ├── repository +│ └── service +├── application +│ ├── project +│ ├── commit +│ └── user +├── infrastructure +│ ├── git +│ ├── shell +│ ├── quartz +│ └── persistence +└── presentation + ├── view + └── rest +``` + +### 3. Внедрение принципов чистой архитектуры + +#### Интерфейсы и контракты +Определить четкие интерфейсы для всех сервисов: + +```kotlin +interface GitService { + fun clone(url: String, path: String, branch: String): GitResult + fun commit(path: String, message: String, author: GitAuthor): GitResult + fun push(path: String, branch: String): GitResult +} + +interface FileService { + fun save(baseDir: Path, fileName: String, content: ByteArray): FileResult + fun createTemp(content: String): FileResult +} +``` + +#### Зависимости внутрь +Внешние слои зависят от внутренних через интерфейсы: + +``` +Presentation -> Application -> Domain <- Infrastructure +``` + +### 4. Улучшение обработки ошибок + +Внедрить типизированную обработку ошибок с использованием sealed классов: + +```kotlin +sealed class CommitError { + object RepositoryNotFound : CommitError() + object InvalidBranch : CommitError() + data class GitCommandFailed(val message: String) : CommitError() + data class FileOperationFailed(val message: String) : CommitError() +} + +typealias CommitResult = Result +``` + +### 5. Конфигурирование через внешние параметры + +Вынести все конфигурационные параметры в application.yml: + +```yaml +app: + git: + timeout: 7 + paths: + data-processors: + base: DataProcessorsExt + erf: erf + epf: epf + code-ext: CodeExt + exchange-rules: EXCHANGE_RULES +``` + +### 6. Внедрение автоматизированного тестирования + +#### Unit-тесты +Создать unit-тесты для всех сервисов с использованием mocking: + +```kotlin +class GitServiceTest { + private val shellExecutor = mockk() + private val gitService = GitServiceImpl(shellExecutor) + + @Test + fun `should clone repository successfully`() { + // Given + every { shellExecutor.executeCommand(any()) } returns "" + + // When + val result = gitService.clone("url", "path", "main") + + // Then + assertThat(result.isSuccess).isTrue() + } +} +``` + +#### Интеграционные тесты +Создать интеграционные тесты с использованием Testcontainers: + +```kotlin +@SpringBootTest +@Testcontainers +class CommitWorkflowIntegrationTest { + @Test + fun `should create commit successfully`() { + // Интеграционный тест для полного workflow + } +} +``` + +## План реализации + +### Краткосрочные улучшения (1-2 недели) +1. Разделение GitWorker на специализированные сервисы +2. Внедрение типизированной обработки ошибок +3. Исправление внедрения зависимостей +4. Улучшение конфигурации путей + +### Среднесрочные улучшения (2-4 недели) +1. Переход к многоуровневой архитектуре +2. Введение unit-тестов +3. Улучшение логирования +4. Внедрение Path API вместо строк + +### Долгосрочные улучшения (1-2 месяца) +1. Полный рефакторинг согласно принципам чистой архитектуры +2. Введение интеграционных тестов +3. Внедрение статического анализа кода +4. Расширение документации + +## Преимущества предлагаемых изменений + +### 1. Улучшенная поддерживаемость +- Четкое разделение ответственностей +- Меньше связности между компонентами +- Проще вносить изменения в отдельные части системы + +### 2. Повышенная тестируемость +- Возможность unit-тестирования каждого компонента +- Легкость создания mock-объектов +- Покрытие тестами критических частей системы + +### 3. Улучшенная расширяемость +- Простота добавления новых функций +- Возможность замены компонентов без изменения остальной системы +- Поддержка различных реализаций через интерфейсы + +### 4. Повышенная надежность +- Типизированная обработка ошибок +- Лучшая обработка исключительных ситуаций +- Предотвращение регрессий через тесты + +### 5. Улучшенная документируемость +- Четкая структура проекта +- Понятные интерфейсы и контракты +- Примеры использования компонентов + +## Заключение + +Предложенные улучшения позволят значительно повысить качество архитектуры проекта Commitet JM, сделав его более поддерживаемым, тестируемым и расширяемым. Рекомендуется начать с краткосрочных улучшений и постепенно переходить к более масштабным изменениям, сохраняя работоспособность приложения на каждом этапе. \ No newline at end of file diff --git a/REFACTORING_PLAN.md b/REFACTORING_PLAN.md new file mode 100644 index 0000000..3267cfd --- /dev/null +++ b/REFACTORING_PLAN.md @@ -0,0 +1,185 @@ +# План рефакторинга проекта + +## Приоритизация задач + +Задачи разделены на 4 приоритета: +- **Высокий приоритет (High)** - критические задачи, которые необходимо выполнить в первую очередь +- **Средний приоритет (Medium)** - важные задачи, которые улучшат архитектуру и поддерживаемость +- **Низкий приоритет (Low)** - полезные задачи, которые можно выполнить позже +- **Опционально (Optional)** - дополнительные улучшения + +## Фаза 1: Критические улучшения (High Priority) + +### 1.1. Разделение GitWorker на специализированные сервисы +**Описание:** Разделить класс GitWorker на несколько специализированных сервисов для улучшения разделения ответственностей. +**Задачи:** +- Создать интерфейс GitService и его реализацию +- Создать интерфейс FileService и его реализацию +- Создать интерфейс OneCService и его реализацию +- Перенести соответствующую логику из GitWorker в новые сервисы +- Обновить зависимости в Committer и других компонентах + +**Оценка:** 8 часов +**Риск:** Средний - затрагивает основную бизнес-логику + +### 1.2. Улучшение обработки ошибок +**Описание:** Внедрить типизированную обработку ошибок вместо исключений. +**Задачи:** +- Создать sealed классы для ошибок каждого сервиса +- Заменить выбрасывание исключений на возврат Result типов +- Обновить вызовы сервисов для обработки новых типов ошибок + +**Оценка:** 6 часов +**Риск:** Средний - затрагивает обработку ошибок по всему приложению + +### 1.3. Исправление внедрения зависимостей +**Описание:** Исправить создание GitWorker напрямую в Committer. +**Задачи:** +- Обновить Committer для использования внедрения зависимостей +- Убедиться, что GitWorker правильно зарегистрирован как Spring Bean + +**Оценка:** 2 часа +**Риск:** Низкий + +## Фаза 2: Улучшения структуры и конфигурации (Medium Priority) + +### 2.1. Переход к многоуровневой архитектуре +**Описание:** Реорганизовать структуру проекта согласно рекомендациям. +**Задачи:** +- Создать новые пакеты для слоев (application, domain, infrastructure, presentation) +- Переместить существующие классы в соответствующие пакеты +- Обновить импорты во всем проекте +- Убедиться, что приложение работает корректно после реорганизации + +**Оценка:** 12 часов +**Риск:** Высокий - затрагивает всю структуру проекта + +### 2.2. Вынесение конфигурации в properties +**Описание:** Вынести жестко заданные пути в конфигурационные файлы. +**Задачи:** +- Создать конфигурационные классы для путей +- Обновить application.yml с новыми параметрами +- Заменить жестко заданные строки на значения из конфигурации + +**Оценка:** 4 часа +**Риск:** Низкий + +### 2.3. Улучшение логирования +**Описание:** Исправить использование логгеров в компонентах. +**Задачи:** +- Обновить ShellExecutor для использования собственного логгера +- Проверить другие компоненты на корректное использование логгеров + +**Оценка:** 2 часа +**Риск:** Низкий + +## Фаза 3: Улучшения кода и тестирование (Medium Priority) + +### 3.1. Улучшение работы с путями файлов +**Описание:** Заменить работу со строками на использование Path API. +**Задачи:** +- Обновить методы работы с путями для использования Path API +- Заменить File на Path где это возможно +- Обновить тесты при необходимости + +**Оценка:** 6 часов +**Риск:** Средний + +### 3.2. Улучшение работы с временными файлами +**Описание:** Внедрить утилитарные методы для автоматического управления временными файлами. +**Задачи:** +- Создать утилитарные методы для работы с временными файлами +- Заменить существующую реализацию на новые методы +- Убедиться, что временные файлы корректно удаляются + +**Оценка:** 3 часа +**Риск:** Низкий + +### 3.3. Введение unit-тестов +**Описание:** Создать базовые unit-тесты для основных сервисов. +**Задачи:** +- Добавить зависимости для тестирования (MockK, JUnit) +- Создать тесты для GitService +- Создать тесты для FileService +- Создать тесты для OneCService +- Создать тесты для ChatHistoryService + +**Оценка:** 10 часов +**Риск:** Низкий + +## Фаза 4: Дополнительные улучшения (Low Priority) + +### 4.1. Введение интеграционных тестов +**Описание:** Создать интеграционные тесты для проверки взаимодействия компонентов. +**Задачи:** +- Настроить Testcontainers для тестовой базы данных +- Создать интеграционные тесты для основных сценариев +- Настроить профиль для интеграционных тестов + +**Оценка:** 8 часов +**Риск:** Низкий + +### 4.2. Добавление статического анализа кода +**Описание:** Внедрить инструменты статического анализа кода. +**Задачи:** +- Добавить Detekt в проект +- Добавить Ktlint в проект +- Настроить правила анализа +- Интегрировать в CI/CD pipeline + +**Оценка:** 4 часа +**Риск:** Низкий + +### 4.3. Улучшение документации +**Описание:** Расширить документацию по проекту. +**Задачи:** +- Расширить ARCHITECTURE.md с описанием новой архитектуры +- Создать документацию по API сервисов +- Добавить примеры использования + +**Оценка:** 6 часов +**Риск:** Низкий + +## Фаза 5: CI/CD улучшения (Optional) + +### 5.1. Расширение GitHub Actions +**Описание:** Добавить тестирование и статический анализ в существующий workflow. +**Задачи:** +- Добавить запуск тестов в workflow +- Добавить статический анализ кода +- Настроить публикацию результатов тестов + +**Оценка:** 3 часа +**Риск:** Низкий + +## Общая оценка + +**Общее время:** 71 час +**Сроки выполнения:** +- Фаза 1: 1-2 недели +- Фаза 2: 2-3 недели +- Фаза 3: 2-3 недели +- Фаза 4: 2-4 недели +- Фаза 5: 1 неделя + +## Рекомендации по выполнению + +1. **Начните с Фазы 1** - критические улучшения, которые немедленно улучшат качество кода +2. **Выполняйте рефакторинг итеративно** - по одному сервису за раз +3. **Поддерживайте работоспособность приложения** - после каждого этапа приложение должно работать +4. **Добавляйте тесты параллельно с рефакторингом** - это поможет избежать регрессий +5. **Документируйте изменения** - обновляйте документацию по мере внесения изменений + +## Риски и способы их минимизации + +1. **Регрессии в функциональности** + - Способ минимизации: Создание тестов перед рефакторингом + +2. **Сложности с интеграцией компонентов** + - Способ минимизации: Постепенная миграция, сохранение обратной совместимости + +3. **Проблемы с производительностью** + - Способ минимизации: Профилирование после каждого этапа + +4. **Сопротивление команды изменениям** + - Способ минимизации: Постепенное внедрение, демонстрация преимуществ \ No newline at end of file diff --git a/build.gradle b/build.gradle index 6fbc20c..5fc1f3e 100644 --- a/build.gradle +++ b/build.gradle @@ -28,8 +28,6 @@ repositories { maven { url 'https://global.repo.jmix.io/repository/public' } - maven { url 'https://repo.spring.io/milestone' } - maven { url 'https://repo.spring.io/snapshot' } maven { name = 'Central Portal Snapshots' url = 'https://central.sonatype.com/repository/maven-snapshots/' @@ -60,8 +58,6 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:null" implementation 'io.jmix.translations:jmix-translations-ru' implementation 'com.vaadin:vaadin-core:24.1.0' // Основная зависимость Vaadin - implementation platform("org.springframework.ai:spring-ai-bom:1.0.0-SNAPSHOT") - implementation 'org.springframework.ai:spring-ai-starter-model-ollama' implementation 'io.jmix.quartz:jmix-quartz-starter' implementation 'io.jmix.quartz:jmix-quartz-flowui-starter' } @@ -79,10 +75,21 @@ test { idea { module { - excludeDirs.addAll(files '.jmix', 'node_modules', 'src/main/frontend/generated/', 'src/main/bundles') + excludeDirs.addAll(files('.jmix', 'node_modules', 'src/main/frontend/generated/', 'src/main/bundles')) } } vaadin { optimizeBundle = false } + +// Установка версии Java для компиляции +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +// Настройка компиляции Kotlin +kotlin { + jvmToolchain(17) +} diff --git a/src/main/kotlin/com/company/commitet_jm/component/ShellExecutor.kt b/src/main/kotlin/com/company/commitet_jm/component/ShellExecutor.kt index 6370d62..ec856cd 100644 --- a/src/main/kotlin/com/company/commitet_jm/component/ShellExecutor.kt +++ b/src/main/kotlin/com/company/commitet_jm/component/ShellExecutor.kt @@ -1,6 +1,5 @@ package com.company.commitet_jm.component -import com.company.commitet_jm.service.GitWorker import org.slf4j.LoggerFactory import org.springframework.stereotype.Component import java.io.File @@ -11,7 +10,7 @@ import java.util.concurrent.TimeUnit class ShellExecutor(var workingDir: File = File("."), var timeout:Long = 1) { companion object { - private val log = LoggerFactory.getLogger(GitWorker::class.java) + private val log = LoggerFactory.getLogger(ShellExecutor::class.java) } fun executeCommand(command: List): String { diff --git a/src/main/kotlin/com/company/commitet_jm/config/QuartzConfig.kt b/src/main/kotlin/com/company/commitet_jm/config/QuartzConfig.kt index d0b7af3..a8e40e8 100644 --- a/src/main/kotlin/com/company/commitet_jm/config/QuartzConfig.kt +++ b/src/main/kotlin/com/company/commitet_jm/config/QuartzConfig.kt @@ -1,6 +1,5 @@ package com.company.commitet_jm.config -import com.company.commitet_jm.service.ai.AiComponent import com.company.commitet_jm.sheduledJob.Committer import org.quartz.* import org.springframework.context.annotation.Bean @@ -31,25 +30,4 @@ open class QuartzConfig { .build() } - @Bean - open fun aiJobDetail(): JobDetail { - return JobBuilder.newJob(AiComponent::class.java) - .withIdentity("AIDialog") - .storeDurably() - .build() - } - - @Bean - open fun aiTrigger(aiJobDetail: JobDetail): Trigger { - return TriggerBuilder.newTrigger() - .forJob(aiJobDetail) - .withIdentity("AITrigger") - .startNow() - .withSchedule( - SimpleScheduleBuilder.simpleSchedule() - .withIntervalInSeconds(5) - .repeatForever() - ) - .build() - } } \ No newline at end of file diff --git a/src/main/kotlin/com/company/commitet_jm/service/GitWorker.kt b/src/main/kotlin/com/company/commitet_jm/service/GitWorker.kt index 1f2c0f4..0e3daf4 100644 --- a/src/main/kotlin/com/company/commitet_jm/service/GitWorker.kt +++ b/src/main/kotlin/com/company/commitet_jm/service/GitWorker.kt @@ -155,15 +155,25 @@ class GitWorker( executor.executeCommand(listOf("git", "add", ".")) // 4. Создаем коммит от указанного пользователя - executor.executeCommand( - listOf( - "git", - "-c", "user.name=${commitInfo.author!!.gitLogin}", - "-c", "user.email=${commitInfo.author!!.email}", - "commit", - "-m", commitInfo.description?.let { escapeShellArgument(it) } + // Создаем временный файл с сообщением коммита + val commitMessage = commitInfo.description ?: "Default commit message" + val tempFile = File.createTempFile("commit-message", ".txt") + tempFile.writeText(commitMessage, Charsets.UTF_8) + + try { + executor.executeCommand( + listOf( + "git", + "-c", "user.name=${commitInfo.author!!.gitLogin}", + "-c", "user.email=${commitInfo.author!!.email}", + "commit", + "-F", tempFile.absolutePath + ) ) - ) + } finally { + // Удаляем временный файл + tempFile.delete() + } log.info("git push start") executor.executeCommand(listOf("git", "push", "--force", "-u", "origin", newBranch)) @@ -204,12 +214,6 @@ class GitWorker( StandardCopyOption.REPLACE_EXISTING ) } - val unpackPath = when (file.getType()){ - REPORT -> "$baseDir\\DataProcessorsExt\\erf" - DATAPROCESSOR -> "$baseDir\\DataProcessorsExt\\epf" - - else -> {""} - } file.getType()?.let { fileType -> val unpackPath = when (fileType) { REPORT -> "$baseDir\\DataProcessorsExt\\erf" diff --git a/src/main/kotlin/com/company/commitet_jm/service/ai/AiComponent.kt b/src/main/kotlin/com/company/commitet_jm/service/ai/AiComponent.kt deleted file mode 100644 index fc8e2f7..0000000 --- a/src/main/kotlin/com/company/commitet_jm/service/ai/AiComponent.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.company.commitet_jm.service.ai - -import io.jmix.core.DataManager -import io.jmix.core.security.SystemAuthenticator -import io.jmix.flowui.UiEventPublisher -import org.quartz.Job -import org.quartz.JobExecutionContext -import org.springframework.ai.chat.model.ChatModel -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.stereotype.Component - -@Component -class AiComponent(private val dataManager: DataManager, - private val uiEventPublisher: UiEventPublisher, - private val chatModel: ChatModel -): Job { - @Autowired - private val systemAuthenticator: SystemAuthenticator? = null - - - - override fun execute(context: JobExecutionContext) { - systemAuthenticator?.runWithSystem { - val ai = AiDialogService(dataManager, uiEventPublisher, chatModel) - ai.aiAnswerMessage() - } - } - -} \ No newline at end of file diff --git a/src/main/kotlin/com/company/commitet_jm/service/ai/AiDialogService.kt b/src/main/kotlin/com/company/commitet_jm/service/ai/AiDialogService.kt deleted file mode 100644 index c91e325..0000000 --- a/src/main/kotlin/com/company/commitet_jm/service/ai/AiDialogService.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.company.commitet_jm.service.ai - -import com.company.commitet_jm.service.ChatHistoryService -import io.jmix.core.DataManager -import io.jmix.flowui.UiEventPublisher -import org.springframework.ai.chat.client.ChatClient -import org.springframework.ai.chat.model.ChatModel - - -import org.springframework.stereotype.Component - - -@Component -class AiDialogService(private val dataManager: DataManager, - private val uiEventPublisher: UiEventPublisher, - private val chatModel: ChatModel) { - - - fun aiAnswerMessage(){ - val history = ChatHistoryService(dataManager,uiEventPublisher) - val req = history.messageToResponse() -// val msg = getQueueMessage(history) - - req?.content = "DONE!!!" - - if (req != null) { - val chatClient: ChatClient = ChatClient.builder(chatModel) - .build() -// - req.content = req.parrentMessage!!.content?.let { - chatClient.prompt("ты помощник разработчика отвечай в рамках проекта") // Set advisor parameters at runtime -// .advisors { advisor -> -// advisor.param("chat_memory_conversation_id", "678") -// .param("chat_memory_response_size", 100) -// } - - .user(it) -// .options(ChatOptions.builder() -// .temperature(0.1) // Very deterministic output -// .build()) - .call() - .content()?.toString() - } ?:"no connection" - - req.generated = true - history.saveResponse(req) - } - - } - -} \ No newline at end of file diff --git a/src/main/kotlin/com/company/commitet_jm/service/file/FileService.kt b/src/main/kotlin/com/company/commitet_jm/service/file/FileService.kt new file mode 100644 index 0000000..5e8447b --- /dev/null +++ b/src/main/kotlin/com/company/commitet_jm/service/file/FileService.kt @@ -0,0 +1,13 @@ +package com.company.commitet_jm.service.file + +import com.company.commitet_jm.entity.FileCommit +import com.company.commitet_jm.entity.Platform +import com.company.commitet_jm.entity.TypesFiles +import java.io.File + +interface FileService { + fun saveFileCommit(baseDir: String, files: MutableList, platform: Platform) + fun correctPath(baseDir: String, type: TypesFiles): File + fun findBinaryFilesFromGitStatus(repoDir: String, executor: com.company.commitet_jm.component.ShellExecutor): List + fun unpackFiles(files: List>, platform: Platform, executor: com.company.commitet_jm.component.ShellExecutor, baseDir: String) +} \ No newline at end of file diff --git a/src/main/kotlin/com/company/commitet_jm/service/file/FileServiceImpl.kt b/src/main/kotlin/com/company/commitet_jm/service/file/FileServiceImpl.kt new file mode 100644 index 0000000..922bc11 --- /dev/null +++ b/src/main/kotlin/com/company/commitet_jm/service/file/FileServiceImpl.kt @@ -0,0 +1,124 @@ +package com.company.commitet_jm.service.file + +import com.company.commitet_jm.component.ShellExecutor +import com.company.commitet_jm.entity.FileCommit +import com.company.commitet_jm.entity.Platform +import com.company.commitet_jm.entity.TypesFiles +import com.company.commitet_jm.service.ones.OneRunner +import io.jmix.core.FileStorage +import io.jmix.core.FileStorageLocator +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service +import java.io.File +import java.io.IOException +import java.nio.file.Files +import java.nio.file.StandardCopyOption + +@Service +class FileServiceImpl( + private val fileStorageLocator: FileStorageLocator, + private val ones: OneRunner +) : FileService { + + companion object { + private val log = LoggerFactory.getLogger(FileServiceImpl::class.java) + } + + override fun saveFileCommit(baseDir: String, files: MutableList, platform: Platform) { + val executor = ShellExecutor(workingDir = File(baseDir), timeout = 7) + val filesToUnpack = mutableListOf>() + for (file in files) { + val content = file.data ?: continue + + // correctPath возвращает File, приводим к Path + val path = file.getType()?.let { correctPath(baseDir, it).toPath() } ?: continue + val targetPath = path.resolve(file.name.toString()).normalize() + + try { + // Создаем директории, если нужно + Files.createDirectories(targetPath.parent) + } catch (e: IOException) { + throw RuntimeException("Не удалось создать директорию: ${targetPath.parent}", e) + } + + val fileStorage = fileStorageLocator.getDefault() + fileStorage.openStream(content).use { inputStream -> + Files.copy( + inputStream, + targetPath, + StandardCopyOption.REPLACE_EXISTING + ) + } + file.getType()?.let { fileType -> + val unpackPath = when (fileType) { + TypesFiles.REPORT -> "$baseDir\\DataProcessorsExt\\erf" + TypesFiles.DATAPROCESSOR -> "$baseDir\\DataProcessorsExt\\epf" + else -> null + } + unpackPath?.let { + filesToUnpack.add(targetPath.toString() to unpackPath) + } + } + + } + if (filesToUnpack.isNotEmpty()) { + unpackFiles(filesToUnpack, platform, executor, baseDir) + } + } + + override fun correctPath(baseDir: String, type: TypesFiles): File { + return when (type) { + TypesFiles.REPORT -> File(baseDir, "DataProcessorsExt\\Отчет\\") + TypesFiles.DATAPROCESSOR -> File(baseDir, "DataProcessorsExt\\Обработка\\") + TypesFiles.SCHEDULEDJOBS -> File(baseDir, "CodeExt") + TypesFiles.EXTERNAL_CODE -> File(baseDir, "CodeExt") + TypesFiles.EXCHANGE_RULES -> File(baseDir, "EXCHANGE_RULES") + } + } + + override fun findBinaryFilesFromGitStatus(repoDir: String, executor: ShellExecutor): List { + // Получаем список изменённых файлов + val gitOutput = executor.executeCommand(listOf("git", "-C", repoDir, "status", "--porcelain")).trim() + if (gitOutput.isBlank()) return emptyList() + + // Выделяем директории из вывода git status + val changedDirs = gitOutput + .lines() + .mapNotNull { line -> + val filePath = line.substringAfter(" ").takeIf { it.isNotBlank() } + filePath?.let { File(repoDir, it) } + } + .filterNotNull() + .distinct() + + // Ищем .bin файлы в изменённых директориях + val tDir = changedDirs.flatMap { dir -> + dir.walk() + .filter { file -> + file.isFile && file.name.endsWith("Form.bin", ignoreCase = false) + } + .toList() + } + return tDir + } + + override fun unpackFiles(files: List>, platform: Platform, executor: ShellExecutor, baseDir: String) { + + if (files.isEmpty()) { + return + } + + for ((sourcePath, unpackPath) in files) { + ones.uploadExtFiles(File(sourcePath), unpackPath, platform.pathInstalled.toString(), platform.version.toString()) + } + + val bFiles = findBinaryFilesFromGitStatus(baseDir, executor) + if (bFiles.isEmpty()) { + return + } + bFiles.forEach { binFile -> + ones.unpackExtFiles(binFile, binFile.parent) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/company/commitet_jm/service/git/GitService.kt b/src/main/kotlin/com/company/commitet_jm/service/git/GitService.kt new file mode 100644 index 0000000..33bd707 --- /dev/null +++ b/src/main/kotlin/com/company/commitet_jm/service/git/GitService.kt @@ -0,0 +1,6 @@ +package com.company.commitet_jm.service.git + +interface GitService { + fun cloneRepo(repoUrl: String, directoryPath: String, branch: String): Pair + fun createCommit() +} \ No newline at end of file diff --git a/src/main/kotlin/com/company/commitet_jm/service/git/GitServiceImpl.kt b/src/main/kotlin/com/company/commitet_jm/service/git/GitServiceImpl.kt new file mode 100644 index 0000000..a370fa5 --- /dev/null +++ b/src/main/kotlin/com/company/commitet_jm/service/git/GitServiceImpl.kt @@ -0,0 +1,258 @@ +package com.company.commitet_jm.service.git + +import com.company.commitet_jm.component.ShellExecutor +import com.company.commitet_jm.entity.* +import com.company.commitet_jm.entity.TypesFiles.* +import com.company.commitet_jm.service.file.FileService +import com.company.commitet_jm.service.ones.OneCService +import io.jmix.core.DataManager +import io.jmix.core.FileStorageLocator +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service +import java.io.File +import java.util.* + +@Service +class GitServiceImpl( + private val dataManager: DataManager, + private val fileStorageLocator: FileStorageLocator, + private val fileService: FileService, + private val oneCService: OneCService +) : GitService { + + companion object { + private val log = LoggerFactory.getLogger(GitServiceImpl::class.java) + } + + override fun cloneRepo(repoUrl: String, directoryPath: String, branch: String): Pair { + val executor = ShellExecutor(timeout = 7) + log.info("start clone repo $repoUrl") + validateGitUrl(repoUrl) + + val dir = File(directoryPath) + + if (dir.exists() && dir.list()?.isNotEmpty() == true) { + throw IllegalArgumentException("Target directory must be empty") + } + + executor.executeCommand(listOf( + "git", "clone", + "--branch", branch, + "--single-branch", + repoUrl, + directoryPath + )) + log.info("end clone repo $repoUrl") + return Pair(true, "") + } + + override fun createCommit() { + val commitInfo = firstNewDataCommit() ?: return + + log.info("start createCommit ${commitInfo.taskNum}") + + val repoPath = commitInfo.project!!.localPath!! + val repoDir = commitInfo.project!!.localPath?.let { File(it) } + val remoteBranch = commitInfo.project!!.defaultBranch + val newBranch = "feature/${commitInfo.taskNum?.let { sanitizeGitBranchName(it) }}" + + try { + if (repoDir != null) { + beforeCmdCommit(repoDir, remoteBranch!!, newBranch, commitInfo) + } else { + throw RuntimeException("$repoDir not exist!!!") + } + + commitInfo.project?.platform?.let { + fileService.saveFileCommit(repoPath, commitInfo.files, it) + } + afterCmdCommit(commitInfo, repoDir, newBranch) + } catch (e: Exception) { + log.error("Error occurred while creating commit ${e.message}") + commitInfo.errorInfo = e.message + commitInfo.setStatus(StatusSheduler.ERROR) + dataManager.save(commitInfo) + } + } + + private fun validateGitUrl(url: String) { + if (!url.matches(Regex("^(https?|git|ssh)://.*"))) { + throw IllegalArgumentException("Invalid Git URL format") + } + } + + private fun beforeCmdCommit(repoDir: File, remoteBranch: String, newBranch: String, commitInfo: Commit) { + if (!File(repoDir, ".git").exists()) { + throw IllegalArgumentException("Not a git repository") + } + val repoPath = commitInfo.project!!.localPath!! + val executor = ShellExecutor(workingDir = repoDir, timeout = 7) + try { + executor.executeCommand(listOf("git", "checkout", remoteBranch)) + } catch (e: Exception) { + + if (e.message?.contains("index.lock") == true) { + log.info("Обнаружена блокировка Git. Удаление index.lock...") + + val lockFile = File("$repoDir/.git/index.lock") + if (lockFile.exists()) { + lockFile.delete() + log.info("Файл index.lock удалён. Повторная попытка...") + + executor.executeCommand(listOf("git", "checkout", remoteBranch)) + } else { + throw IllegalStateException("Файл блокировки не найден, но ошибка возникла: ${e.message}") + } + } else if (e.message?.contains("would be overwritten by checkout") == true) { + + executor.executeCommand(listOf("git", "reset", "--hard")) + executor.executeCommand(listOf("git", "clean", "-fd")) + + } else { + throw e + } + } + + executor.executeCommand(listOf("git", "reset", "--hard", "origin/$remoteBranch")) + executor.executeCommand(listOf("git", "clean", "-fd")) + + commitInfo.id?.let { setStatusCommit(it, StatusSheduler.PROCESSED) } + + val remoteBranches = executor.executeCommand(listOf("git", "branch", "-a")) + + if (!remoteBranches.contains("remotes/origin/$remoteBranch")) { + throw IllegalStateException("Default branch does not exist") + } + + try { + executor.executeCommand(listOf("git", "checkout", remoteBranch)) + executor.executeCommand(listOf("git", "fetch", "origin", remoteBranch)) + executor.executeCommand(listOf("git", "checkout", remoteBranch)) + + // Создаем новую ветку + if (branchExists(repoPath, newBranch)) { + log.info("create new branch $newBranch") + executor.executeCommand(listOf("git", "checkout", newBranch)) + } else { + log.info("checkout branch $newBranch") + executor.executeCommand(listOf("git", "checkout", "-b", newBranch)) + } + } catch (e: Exception) { + log.error("beforeCmdCommit ${e.message}") + throw RuntimeException("Error cmd git ${e.message}") + } + } + + private fun afterCmdCommit(commitInfo: Commit, repoDir: File, newBranch: String) { + val executor = ShellExecutor(workingDir = repoDir, timeout = 7) + executor.executeCommand(listOf("git", "add", ".")) + + // 4. Создаем коммит от указанного пользователя + // Создаем временный файл с сообщением коммита + val commitMessage = commitInfo.description ?: "Default commit message" + val tempFile = File.createTempFile("commit-message", ".txt") + tempFile.writeText(commitMessage, Charsets.UTF_8) + + try { + executor.executeCommand( + listOf( + "git", + "-c", "user.name=${commitInfo.author!!.gitLogin}", + "-c", "user.email=${commitInfo.author!!.email}", + "commit", + "-F", tempFile.absolutePath + ) + ) + } finally { + // Удаляем временный файл + tempFile.delete() + } + + log.info("git push start") + executor.executeCommand(listOf("git", "push", "--force", "-u", "origin", newBranch)) + log.info("git push end") + commitInfo.hashCommit = executor.executeCommand(listOf("git", "rev-parse", "HEAD")) + + log.info("Successfully committed and pushed changes to branch $newBranch") + + commitInfo.urlBranch = "${commitInfo.project!!.urlRepo}/tree/$newBranch" + + commitInfo.setStatus(StatusSheduler.COMPLETE) + dataManager.save(commitInfo) + + } + + private fun setStatusCommit(commitId: UUID, status: StatusSheduler) { + val commit = dataManager.load(Commit::class.java) + .id(commitId) + .one() + commit.setStatus(status) + dataManager.save(commit) + } + + private fun branchExists(repoPath: String, branchName: String): Boolean { + val executor = ShellExecutor(workingDir = File(repoPath)) + val branches = executor.executeCommand(listOf("git", "branch", "--list", branchName)) + + return branches.isNotBlank() + } + + private fun firstNewDataCommit(): Commit? { + val entity = dataManager.load(Commit::class.java) + .query("select cmt from Commit_ cmt where cmt.status = :status1 order by cmt.id asc") + .parameter("status1", StatusSheduler.NEW) + .optional() + + if (entity.isEmpty) { + return null + } else { + val commit = entity.get() + commit.author = entity.get().author + commit.files = entity.get().files + commit.project = entity.get().project + + return commit + } + + } + + private fun escapeShellArgument(arg: String): String { + if (arg.isEmpty()) { + return "''" + } + + // Если строка содержит пробелы, кавычки или другие спецсимволы, заключаем её в кавычки + if (!arg.matches(Regex("^[a-zA-Z0-9_\\-+=%/:.,@]+$"))) { + return "'" + arg.replace("'", "'\"'\"'") + "'" + } + + return arg + } + + private fun sanitizeGitBranchName(input: String): String { + // Правила для имён веток Git: + // - Не могут начинаться с '-' + // - Не могут содержать: + // - пробелы + // - символы: ~, ^, :, *, ?, [, ], @, \, /, {, }, ... + // - Не могут заканчиваться на .lock + // - Не могут содержать последовательность // + + val forbiddenChars = setOf( + ' ', '~', '^', ':', '*', '?', '[', ']', '@', '\\', '/', '{', '}', + '<', '>', '|', '"', '\'', '!', '#', '$', '%', '&', '(', ')', ',', ';', '=' + ) + + return input.map { char -> + when { + char in forbiddenChars -> '_' + else -> char + } + }.joinToString("") + .removePrefix(".") // Убираем точку в начале (если есть) + .replace(Regex("[/\\\\]+"), "_") // Заменяем несколько / или \ на один _ + .replace(Regex("[._]{2,}"), "_") // Заменяем несколько точек или _ подряд на один _ + .replace(Regex("_+$"), "") // Убираем _ в конце + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/company/commitet_jm/service/ones/OneCService.kt b/src/main/kotlin/com/company/commitet_jm/service/ones/OneCService.kt new file mode 100644 index 0000000..5b0e83d --- /dev/null +++ b/src/main/kotlin/com/company/commitet_jm/service/ones/OneCService.kt @@ -0,0 +1,9 @@ +package com.company.commitet_jm.service.ones + +import java.io.File + +interface OneCService { + fun uploadExtFiles(inputFile: File, outDir: String, pathInstall: String, version: String) + fun unpackExtFiles(inputFile: File, outDir: String) + fun pathPlatform(basePath: String?, version: String?): String +} \ No newline at end of file diff --git a/src/main/kotlin/com/company/commitet_jm/service/ones/OneCServiceImpl.kt b/src/main/kotlin/com/company/commitet_jm/service/ones/OneCServiceImpl.kt new file mode 100644 index 0000000..440a106 --- /dev/null +++ b/src/main/kotlin/com/company/commitet_jm/service/ones/OneCServiceImpl.kt @@ -0,0 +1,104 @@ +package com.company.commitet_jm.service.ones + +import com.company.commitet_jm.component.ShellExecutor +import com.company.commitet_jm.entity.AppSettings +import io.jmix.core.DataManager +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service +import java.io.File + +@Service +class OneCServiceImpl( + private val dataManager: DataManager, + private val shellExecutor: ShellExecutor +) : OneCService { + + companion object { + private val log = LoggerFactory.getLogger(OneCServiceImpl::class.java) + } + + private var v8unpackPath: String = "" + + override fun uploadExtFiles(inputFile: File, outDir: String, pathInstall: String, version: String) { + val res = shellExecutor.executeCommand(listOf( + pathPlatform(pathInstall, version), + "DESIGNER", + "/DumpExternalDataProcessorOrReportToFiles", + "\"$outDir\"", + "\"${inputFile.path}\"" + )) + log.debug("Строка запуска $res") + } + + override fun unpackExtFiles(inputFile: File, outDir: String) { + if (v8unpackPath.isEmpty()) { + val unpackPath = dataManager.load(AppSettings::class.java) + .query("select apps from AppSettings apps where apps.name = :pName") + .parameter("pName", "v8unpack") + .optional().get() + + v8unpackPath = unpackPath.value.toString() + } + + val res = shellExecutor.executeCommand(listOf( + v8unpackPath, + "-U", + inputFile.path, + outDir + )) + + log.info("unpack rename files") + + filterAndRenameFiles( + directory = File(outDir), + keepFiles = setOf("form.data", "module.data", "Form.bin"), + renameRule = { originalName -> + when (originalName) { + "form.data" -> "form" + "module.data" -> "Module.bsl" + else -> { + originalName + } + } + } + ) + log.debug("Unpack command $res") + } + + override fun pathPlatform(basePath: String?, version: String?): String { + return "$basePath\\$version\\bin\\1cv8.exe" + } + + /** + * Оставляет в директории только указанные файлы, переименовывает их и удаляет остальные + * @param directory Целевая директория + * @param keepFiles Список имён файлов для сохранения (регистрозависимый) + * @param renameRule Функция для генерации нового имени файла на основе старого + */ + private fun filterAndRenameFiles( + directory: File, + keepFiles: Set, + renameRule: (String) -> String + ) { + if (!directory.isDirectory) return + + directory.listFiles()?.forEach { file -> + when { + file.isFile && file.name in keepFiles -> { + val newName = renameRule(file.name) + val newFile = File(directory, newName) + if (newName != file.name) { + if (newFile.exists()) newFile.delete() + file.renameTo(newFile) + } + } + else -> { + if (!file.isDirectory) { + file.delete() + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/company/commitet_jm/service/ones/OneCStorage.kt b/src/main/kotlin/com/company/commitet_jm/service/ones/OneCStorage.kt index 8f85636..d6b70f8 100644 --- a/src/main/kotlin/com/company/commitet_jm/service/ones/OneCStorage.kt +++ b/src/main/kotlin/com/company/commitet_jm/service/ones/OneCStorage.kt @@ -6,7 +6,6 @@ package com.company.commitet_jm.service.ones import com.company.commitet_jm.component.ShellExecutor import com.company.commitet_jm.entity.OneCStorage import com.company.commitet_jm.entity.Platform -import com.company.commitet_jm.service.GitWorker import com.company.commitet_jm.view.onecstorage.HistoryOptions import org.slf4j.LoggerFactory import org.springframework.stereotype.Component @@ -217,6 +216,7 @@ private fun buildHistoryCommand( + private fun executeConfigurationCommand(storage: OneCStorage, commandParts: List) { try { val baseCommand = listOf( @@ -253,11 +253,14 @@ private fun getPlatformPath(storage: OneCStorage): String { + + // создать нового пользователя // /ConfigurationRepositoryAddUser + } private fun buildAddUserCommand( diff --git a/src/main/kotlin/com/company/commitet_jm/service/ones/OneRunner.kt b/src/main/kotlin/com/company/commitet_jm/service/ones/OneRunner.kt index 096081f..5d74fed 100644 --- a/src/main/kotlin/com/company/commitet_jm/service/ones/OneRunner.kt +++ b/src/main/kotlin/com/company/commitet_jm/service/ones/OneRunner.kt @@ -2,7 +2,6 @@ package com.company.commitet_jm.service.ones import com.company.commitet_jm.component.ShellExecutor import com.company.commitet_jm.entity.AppSettings -import com.company.commitet_jm.service.GitWorker import io.jmix.core.DataManager import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired @@ -13,7 +12,7 @@ class OneRunner(private val dataManager: DataManager ) { companion object { - private val log = LoggerFactory.getLogger(GitWorker::class.java) + private val log = LoggerFactory.getLogger(OneRunner::class.java) } @Autowired private lateinit var shellExecutor: ShellExecutor diff --git a/src/main/kotlin/com/company/commitet_jm/sheduledJob/Committer.kt b/src/main/kotlin/com/company/commitet_jm/sheduledJob/Committer.kt index dfe9319..c472f00 100644 --- a/src/main/kotlin/com/company/commitet_jm/sheduledJob/Committer.kt +++ b/src/main/kotlin/com/company/commitet_jm/sheduledJob/Committer.kt @@ -1,8 +1,7 @@ package com.company.commitet_jm.sheduledJob -import com.company.commitet_jm.service.GitWorker +import com.company.commitet_jm.service.git.GitService import io.jmix.core.DataManager -import io.jmix.core.FileStorageLocator import io.jmix.core.security.SystemAuthenticator import org.quartz.Job import org.quartz.JobExecutionContext @@ -11,20 +10,16 @@ import org.springframework.stereotype.Component @Component -class Committer(private val dataManager: DataManager): Job { +class Committer( + private val dataManager: DataManager, + private val gitService: GitService +): Job { @Autowired private val systemAuthenticator: SystemAuthenticator? = null - @Autowired - private lateinit var fileStorageLocator: FileStorageLocator - override fun execute(context: JobExecutionContext) { systemAuthenticator?.runWithSystem { - val gitWorker = GitWorker( - dataManager = dataManager, - fileStorageLocator = fileStorageLocator, - ) - gitWorker.createCommit() + gitService.createCommit() } } } \ No newline at end of file diff --git a/src/main/kotlin/com/company/commitet_jm/sheduledJob/GitCloneTask.kt b/src/main/kotlin/com/company/commitet_jm/sheduledJob/GitCloneTask.kt index 1a49b26..fa68c7a 100644 --- a/src/main/kotlin/com/company/commitet_jm/sheduledJob/GitCloneTask.kt +++ b/src/main/kotlin/com/company/commitet_jm/sheduledJob/GitCloneTask.kt @@ -1,6 +1,6 @@ package com.company.commitet_jm.sheduledJob -import com.company.commitet_jm.service.GitWorker +import com.company.commitet_jm.service.git.GitService import io.jmix.core.DataManager import io.jmix.core.FileStorageLocator import io.jmix.flowui.backgroundtask.BackgroundTask @@ -13,13 +13,14 @@ import java.util.concurrent.TimeUnit class GitCloneTask( private val dataManager: DataManager, private val fileStorageLocator: FileStorageLocator, + private val gitService: GitService ) : BackgroundTask( 10, TimeUnit.MINUTES ) { companion object { - private val log = LoggerFactory.getLogger(GitWorker::class.java) + private val log = LoggerFactory.getLogger(GitCloneTask::class.java) } var urlRepo: String = "" var localPath: String = "" @@ -27,9 +28,7 @@ class GitCloneTask( @Throws(Exception::class) override fun run(taskLifeCycle: TaskLifeCycle): Void? { - val gw = GitWorker(dataManager = dataManager, fileStorageLocator = fileStorageLocator) - - val result = gw.cloneRepo("$urlRepo.git", localPath, defaultBranch) + val result = gitService.cloneRepo("$urlRepo.git", localPath, defaultBranch) if (!result.first) { log.error("Ошибка клонирования: ${result.second}") diff --git a/src/main/kotlin/com/company/commitet_jm/view/commit/CommitDetailView.kt b/src/main/kotlin/com/company/commitet_jm/view/commit/CommitDetailView.kt index 6a07486..a8121fe 100644 --- a/src/main/kotlin/com/company/commitet_jm/view/commit/CommitDetailView.kt +++ b/src/main/kotlin/com/company/commitet_jm/view/commit/CommitDetailView.kt @@ -1,7 +1,7 @@ package com.company.commitet_jm.view.commit import com.company.commitet_jm.entity.* -import com.company.commitet_jm.service.GitWorker +import com.company.commitet_jm.service.git.GitService import com.company.commitet_jm.view.main.MainView import com.vaadin.flow.component.ClickEvent import com.vaadin.flow.component.button.Button @@ -37,6 +37,9 @@ class CommitDetailView : StandardDetailView() { @Autowired private lateinit var dataManager: DataManager + @Autowired + private lateinit var gitService: GitService + @ViewComponent private lateinit var errorInfoField: JmixTextArea @@ -140,11 +143,7 @@ class CommitDetailView : StandardDetailView() { @Subscribe(id = "uploadFilesButton", subject = "clickListener") private fun onUploadFilesButtonCommitClick(event: ClickEvent) { - val gitWorker = GitWorker( - dataManager = dataManager, - fileStorageLocator = fileStorageLocator, - ) - gitWorker.createCommit() + gitService.createCommit() } diff --git a/src/main/kotlin/com/company/commitet_jm/view/commit/CommitListView.kt b/src/main/kotlin/com/company/commitet_jm/view/commit/CommitListView.kt index cf8de22..e698045 100644 --- a/src/main/kotlin/com/company/commitet_jm/view/commit/CommitListView.kt +++ b/src/main/kotlin/com/company/commitet_jm/view/commit/CommitListView.kt @@ -3,7 +3,7 @@ package com.company.commitet_jm.view.commit import com.company.commitet_jm.entity.Commit import com.company.commitet_jm.entity.StatusSheduler import com.company.commitet_jm.entity.User -import com.company.commitet_jm.service.GitWorker +import com.company.commitet_jm.service.git.GitService import com.company.commitet_jm.view.main.MainView import com.vaadin.flow.component.ClickEvent import com.vaadin.flow.component.html.Span @@ -45,6 +45,9 @@ class CommitListView : StandardListView() { @Autowired private lateinit var dataManager: DataManager + @Autowired + private lateinit var gitService: GitService + @ViewComponent private val commitsDl: CollectionLoader? = null @@ -55,12 +58,7 @@ class CommitListView : StandardListView() { @Subscribe(id = "CreateCommitButton") private fun onCreateCommitButtonClick(event: ClickEvent) { - val gitWorker = GitWorker( - dataManager = dataManager, - fileStorageLocator = fileStorageLocator, - ) - gitWorker.createCommit() - + gitService.createCommit() } @Subscribe diff --git a/src/main/kotlin/com/company/commitet_jm/view/project/ProjectListView.kt b/src/main/kotlin/com/company/commitet_jm/view/project/ProjectListView.kt index f2428c4..4bf116b 100644 --- a/src/main/kotlin/com/company/commitet_jm/view/project/ProjectListView.kt +++ b/src/main/kotlin/com/company/commitet_jm/view/project/ProjectListView.kt @@ -2,6 +2,7 @@ package com.company.commitet_jm.view.project import com.company.commitet_jm.sheduledJob.GitCloneTask import com.company.commitet_jm.entity.Project +import com.company.commitet_jm.service.git.GitService import com.company.commitet_jm.view.main.MainView import com.vaadin.flow.component.ClickEvent import com.vaadin.flow.component.HasValueAndElement @@ -41,6 +42,9 @@ class ProjectListView : StandardListView() { @Autowired private lateinit var dataManager: DataManager + @Autowired + private lateinit var gitService: GitService + @ViewComponent private lateinit var dataContext: DataContext @@ -124,7 +128,8 @@ class ProjectListView : StandardListView() { val task = GitCloneTask( dataManager = dataManager, - fileStorageLocator = fileStorageLocator + fileStorageLocator = fileStorageLocator, + gitService = gitService ).apply { urlRepo = urlRepoField.value localPath = localPathField.value diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 40d4f33..a1270b4 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -47,6 +47,3 @@ spring.quartz.jdbc.initialize-schema = always spring.quartz.properties.org.quartz.scheduler.instanceName=MyScheduler spring.quartz.properties.org.quartz.threadPool.threadCount=5 spring.servlet.multipart.max-file-size=30MB - -spring.ai.ollama.base-url = http://localhost:11434 -spring.ai.ollama.chat.model=codellama:7b \ No newline at end of file