diff --git a/.idea/gradle.xml b/.idea/gradle.xml index b3e8a2e..02ff145 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -11,6 +11,7 @@ diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts index 940daf9..6979a1a 100644 --- a/cli/build.gradle.kts +++ b/cli/build.gradle.kts @@ -11,6 +11,7 @@ repositories { dependencies { implementation(project(":core")) + implementation(project(":git")) implementation(kotlin("stdlib")) testImplementation(libs.mockk) testImplementation(libs.junit4) diff --git a/cli/src/main/kotlin/com/mitteloupe/cag/cli/AppArgumentProcessor.kt b/cli/src/main/kotlin/com/mitteloupe/cag/cli/AppArgumentProcessor.kt index 55d6d73..51e720c 100644 --- a/cli/src/main/kotlin/com/mitteloupe/cag/cli/AppArgumentProcessor.kt +++ b/cli/src/main/kotlin/com/mitteloupe/cag/cli/AppArgumentProcessor.kt @@ -70,7 +70,8 @@ class AppArgumentProcessor(private val argumentParser: ArgumentParser = Argument featureName = secondaries[SecondaryFlagConstants.NAME].orEmpty(), packageName = secondaries[SecondaryFlagConstants.PACKAGE], enableKtlint = secondaries.containsKey(SecondaryFlagConstants.KTLINT), - enableDetekt = secondaries.containsKey(SecondaryFlagConstants.DETEKT) + enableDetekt = secondaries.containsKey(SecondaryFlagConstants.DETEKT), + enableGit = secondaries.containsKey(SecondaryFlagConstants.GIT) ) } @@ -85,7 +86,8 @@ class AppArgumentProcessor(private val argumentParser: ArgumentParser = Argument DataSourceRequest( dataSourceName = name, useKtor = libraries.contains("ktor"), - useRetrofit = libraries.contains("retrofit") + useRetrofit = libraries.contains("retrofit"), + enableGit = secondaries.containsKey(SecondaryFlagConstants.GIT) ) } @@ -109,7 +111,8 @@ class AppArgumentProcessor(private val argumentParser: ArgumentParser = Argument ) { secondaries -> ViewModelRequest( viewModelName = secondaries[SecondaryFlagConstants.NAME].orEmpty(), - targetPath = secondaries[SecondaryFlagConstants.PATH] + targetPath = secondaries[SecondaryFlagConstants.PATH], + enableGit = secondaries.containsKey(SecondaryFlagConstants.GIT) ) } @@ -121,7 +124,8 @@ class AppArgumentProcessor(private val argumentParser: ArgumentParser = Argument ArchitectureRequest( enableCompose = !secondaries.containsKey(SecondaryFlagConstants.NO_COMPOSE), enableKtlint = secondaries.containsKey(SecondaryFlagConstants.KTLINT), - enableDetekt = secondaries.containsKey(SecondaryFlagConstants.DETEKT) + enableDetekt = secondaries.containsKey(SecondaryFlagConstants.DETEKT), + enableGit = secondaries.containsKey(SecondaryFlagConstants.GIT) ) } @@ -137,7 +141,8 @@ class AppArgumentProcessor(private val argumentParser: ArgumentParser = Argument enableKtlint = secondaries.containsKey(SecondaryFlagConstants.KTLINT), enableDetekt = secondaries.containsKey(SecondaryFlagConstants.DETEKT), enableKtor = secondaries.containsKey(SecondaryFlagConstants.KTOR), - enableRetrofit = secondaries.containsKey(SecondaryFlagConstants.RETROFIT) + enableRetrofit = secondaries.containsKey(SecondaryFlagConstants.RETROFIT), + enableGit = secondaries.containsKey(SecondaryFlagConstants.GIT) ) } diff --git a/cli/src/main/kotlin/com/mitteloupe/cag/cli/HelpContent.kt b/cli/src/main/kotlin/com/mitteloupe/cag/cli/HelpContent.kt index 193da95..667eaea 100644 --- a/cli/src/main/kotlin/com/mitteloupe/cag/cli/HelpContent.kt +++ b/cli/src/main/kotlin/com/mitteloupe/cag/cli/HelpContent.kt @@ -1,13 +1,22 @@ package com.mitteloupe.cag.cli object HelpContent { + private const val NEW_PROJECT_SYNTAX = + "[--new-project --name=ProjectName --package=PackageName " + + "[--no-compose] [--ktlint] [--detekt] [--ktor] [--retrofit] [--git]]" + private const val NEW_ARCHITECTURE_SYNTAX = "[--new-architecture [--no-compose] [--ktlint] [--detekt] [--git]]" + private const val NEW_FEATURE_SYNTAX = "[--new-feature --name=FeatureName [--package=PackageName] [--ktlint] [--detekt] [--git]]" + private const val NEW_DATASOURCE_SYNTAX = "[--new-datasource --name=DataSourceName [--with=ktor|retrofit|ktor,retrofit] [--git]]" + private const val NEW_USE_CASE_SYNTAX = "[--new-use-case --name=UseCaseName [--path=TargetPath] [--git]]" + private const val NEW_VIEW_MODEL_SYNTAX = "[--new-view-model --name=ViewModelName [--path=TargetPath] [--git]]" const val USAGE_SYNTAX: String = "usage: cag " + - "[--new-project --name=ProjectName --package=PackageName [--no-compose] [--ktlint] [--detekt] [--ktor] [--retrofit]]... " + - "[--new-architecture [--no-compose] [--ktlint] [--detekt]]... " + - "[--new-feature --name=FeatureName [--package=PackageName] [--ktlint] [--detekt]]... " + - "[--new-datasource --name=DataSourceName [--with=ktor|retrofit|ktor,retrofit]]... " + - "[--new-use-case --name=UseCaseName [--path=TargetPath]]... [--new-view-model --name=ViewModelName [--path=TargetPath]]..." + "$NEW_PROJECT_SYNTAX... " + + "$NEW_ARCHITECTURE_SYNTAX... " + + "$NEW_FEATURE_SYNTAX... " + + "$NEW_DATASOURCE_SYNTAX... " + + "$NEW_USE_CASE_SYNTAX... " + + "$NEW_VIEW_MODEL_SYNTAX..." fun helpSections(): Map = buildMap { @@ -22,20 +31,23 @@ object HelpContent { --package=PackageName | --package PackageName | -p=PackageName | -p PackageName | -pPackageName Specify the package name (required) --no-compose | -nc - Disable Compose support for the project + Disable Compose support for the project --ktlint | -kl - Enable ktlint for the project + Enable ktlint for the project --detekt | -d - Enable detekt for the project + Enable detekt for the project --ktor | -kt - Enable Ktor for data sources + Enable Ktor for data sources --retrofit | -rt - Enable Retrofit for data sources + Enable Retrofit for data sources + --git | -g + Automatically initialize git repository and stage changes Examples: cag --new-project --name=MyApp --package=com.example.myapp cag --new-project --name=MyApp --package=com.example.myapp --no-compose --ktlint --detekt cag --new-project --name=MyApp --package=com.example.myapp --ktor --retrofit + cag --new-project --name=MyApp --package=com.example.myapp --git """.trimIndent() ) put( @@ -45,16 +57,19 @@ object HelpContent { --new-architecture | -na Generate a new Clean Architecture package with domain, presentation, and UI layers --no-compose | -nc - Disable Compose support for the preceding architecture package + Disable Compose support for the preceding architecture package --ktlint | -kl - Enable ktlint for the preceding architecture package + Enable ktlint for the preceding architecture package --detekt | -d - Enable detekt for the preceding architecture package + Enable detekt for the preceding architecture package + --git | -g + Automatically stage changes to git repository Examples: cag --new-architecture cag --new-architecture --no-compose cag --new-architecture --ktlint --detekt + cag --new-architecture --git """.trimIndent() ) put( @@ -68,9 +83,11 @@ object HelpContent { --package=PackageName | --package PackageName | -p=PackageName | -p PackageName | -pPackageName Override the feature package for the preceding feature --ktlint | -kl - Enable ktlint for the preceding feature (adds plugin and .editorconfig if missing) + Enable ktlint for the preceding feature (adds plugin and .editorconfig if missing) --detekt | -d - Enable detekt for the preceding feature (adds plugin and detekt.yml if missing) + Enable detekt for the preceding feature (adds plugin and detekt.yml if missing) + --git | -g + Automatically stage changes to git repository Examples: cag --new-feature --name=Profile @@ -88,6 +105,8 @@ object HelpContent { Specify the data source name (required, DataSource suffix will be added automatically) --with=ktor|retrofit|ktor,retrofit | -w=ktor|retrofit|ktor,retrofit Attach dependencies to the preceding new data source + --git | -g + Automatically stage changes to git repository Examples: cag --new-datasource --name=User @@ -121,6 +140,8 @@ object HelpContent { Specify the ViewModel name (required) --path=TargetPath | --path TargetPath | -p=TargetPath | -p TargetPath | -pTargetPath Specify the target directory for the preceding ViewModel + --git | -g + Automatically stage changes to git repository Examples: cag --new-view-model --name=Profile @@ -155,10 +176,17 @@ object HelpContent { - Values in the project .cagrc override values in ~/.cagrc. Sections: - - [new.versions] — applied when generating new projects (e.g., --new-project) - - [existing.versions] — applied when generating into an existing project (e.g., new architecture, feature, data source, use case, or view model) + - [new.versions] - applied when generating new projects (e.g., --new-project) + - [existing.versions] - applied when generating into an existing project (e.g., new architecture, feature, data source, use case, or view model) + - [git] - configuration for git integration - Keys correspond to version keys used by the generator, for example: kotlin, androidGradlePlugin, composeBom, composeNavigation, retrofit, ktor, okhttp3, etc. + Version Keys: + - Keys in [new.versions] and [existing.versions] correspond to version keys used by the generator, + for example: kotlin, androidGradlePlugin, composeBom, composeNavigation, retrofit, ktor, okhttp3, etc. + + Git Configuration: + - autoInitialize=true|false - Whether to automatically initialize a git repository for new projects (default: false) + - autoStage=true|false - Whether to automatically stage changes after generation (default: true) Example ~/.cagrc: [new.versions] @@ -169,12 +197,19 @@ object HelpContent { retrofit=2.11.0 ktor=3.0.3 + [git] + autoInitialize=true + autoStage=true + Example ./.cagrc (project overrides): [new.versions] composeBom=2025.09.01 [existing.versions] okhttp3=4.12.0 + + [git] + autoInitialize=false """.trimIndent() ) } diff --git a/cli/src/main/kotlin/com/mitteloupe/cag/cli/Main.kt b/cli/src/main/kotlin/com/mitteloupe/cag/cli/Main.kt index b7ff430..9e72082 100644 --- a/cli/src/main/kotlin/com/mitteloupe/cag/cli/Main.kt +++ b/cli/src/main/kotlin/com/mitteloupe/cag/cli/Main.kt @@ -33,6 +33,7 @@ import com.mitteloupe.cag.core.request.GenerateFeatureRequestBuilder import com.mitteloupe.cag.core.request.GenerateProjectTemplateRequest import com.mitteloupe.cag.core.request.GenerateUseCaseRequest import com.mitteloupe.cag.core.request.GenerateViewModelRequest +import com.mitteloupe.cag.git.Git import java.io.File import java.nio.file.Paths import kotlin.system.exitProcess @@ -83,6 +84,8 @@ fun main(arguments: Array) { val destinationRootDirectory = projectModel.selectedModuleRootDir() ?: projectRoot val projectNamespace = basePackage ?: "com.unknown.app." + val git = Git() + projectTemplateRequests.forEach { request -> val projectTemplateDestinationDirectory = if (projectModel.selectedModuleRootDir() != null) { @@ -105,6 +108,14 @@ fun main(arguments: Array) { setVersionProvider(configuration.newProjectVersions) generator.generateProjectTemplate(projectTemplateRequest) } + + val shouldInitGit = request.enableGit || configuration.git.autoInitialize == true + if (shouldInitGit) { + val didInit = git.initializeRepository(projectTemplateDestinationDirectory) + if (!didInit || configuration.git.autoStage == true) { + runCatching { git.stageAll(projectTemplateDestinationDirectory) } + } + } } architectureRequests.forEach { request -> @@ -121,6 +132,11 @@ fun main(arguments: Array) { setVersionProvider(configuration.existingProjectVersions) generator.generateArchitecture(architectureRequest) } + + if (request.enableGit || configuration.git.autoStage == true) { + val gitRoot = projectModel.selectedModuleRootDir() ?: projectRoot + runCatching { git.stageAll(gitRoot) } + } } featureRequests.forEach { requestFeature -> @@ -140,6 +156,11 @@ fun main(arguments: Array) { setVersionProvider(configuration.existingProjectVersions) generator.generateFeature(request) } + + if (requestFeature.enableGit || configuration.git.autoStage == true) { + val gitRoot = projectModel.selectedModuleRootDir() ?: projectRoot + runCatching { git.stageAll(gitRoot) } + } } dataSourceRequests.forEach { request -> @@ -153,6 +174,11 @@ fun main(arguments: Array) { useRetrofit = request.useRetrofit ) } + + if (request.enableGit || configuration.git.autoStage == true) { + val gitRoot = projectModel.selectedModuleRootDir() ?: projectRoot + runCatching { git.stageAll(gitRoot) } + } } useCaseRequests.forEach { request -> @@ -191,6 +217,11 @@ fun main(arguments: Array) { setVersionProvider(configuration.existingProjectVersions) generator.generateViewModel(viewModelRequest) } + + if (request.enableGit || configuration.git.autoStage == true) { + val gitRoot = projectModel.selectedModuleRootDir() ?: projectRoot + runCatching { git.stageAll(gitRoot) } + } } } diff --git a/cli/src/main/kotlin/com/mitteloupe/cag/cli/configuration/ClientConfiguration.kt b/cli/src/main/kotlin/com/mitteloupe/cag/cli/configuration/ClientConfiguration.kt index a86d48a..3109366 100644 --- a/cli/src/main/kotlin/com/mitteloupe/cag/cli/configuration/ClientConfiguration.kt +++ b/cli/src/main/kotlin/com/mitteloupe/cag/cli/configuration/ClientConfiguration.kt @@ -1,8 +1,14 @@ package com.mitteloupe.cag.cli.configuration +data class GitConfiguration( + val autoInitialize: Boolean? = null, + val autoStage: Boolean? = null +) + data class ClientConfiguration( val newProjectVersions: Map = emptyMap(), - val existingProjectVersions: Map = emptyMap() + val existingProjectVersions: Map = emptyMap(), + val git: GitConfiguration = GitConfiguration() ) { companion object { val EMPTY = ClientConfiguration() diff --git a/cli/src/main/kotlin/com/mitteloupe/cag/cli/configuration/ClientConfigurationLoader.kt b/cli/src/main/kotlin/com/mitteloupe/cag/cli/configuration/ClientConfigurationLoader.kt index 3ccdb05..fb8f125 100644 --- a/cli/src/main/kotlin/com/mitteloupe/cag/cli/configuration/ClientConfigurationLoader.kt +++ b/cli/src/main/kotlin/com/mitteloupe/cag/cli/configuration/ClientConfigurationLoader.kt @@ -15,7 +15,12 @@ class ClientConfigurationLoader { projectFile: File?, homeFile: File? ): ClientConfiguration { - val homeConfiguration = if (homeFile?.exists() == true && homeFile.isFile) parse(homeFile.readText()) else ClientConfiguration.EMPTY + val homeConfiguration = + if (homeFile?.exists() == true && homeFile.isFile) { + parse(homeFile.readText()) + } else { + ClientConfiguration.EMPTY + } val projectConfiguration = if (projectFile?.exists() == true && projectFile.isFile) { parse(projectFile.readText()) @@ -31,39 +36,70 @@ class ClientConfigurationLoader { ): ClientConfiguration = ClientConfiguration( newProjectVersions = baseConfiguration.newProjectVersions + override.newProjectVersions, - existingProjectVersions = baseConfiguration.existingProjectVersions + override.existingProjectVersions + existingProjectVersions = baseConfiguration.existingProjectVersions + override.existingProjectVersions, + git = + GitConfiguration( + autoInitialize = override.git.autoInitialize ?: baseConfiguration.git.autoInitialize, + autoStage = override.git.autoStage ?: baseConfiguration.git.autoStage + ) ) - internal fun parse(text: String): ClientConfiguration { - val newProjectVersions = mutableMapOf() - val existingProjectVersions = mutableMapOf() + internal fun parse(text: String): ClientConfiguration = + ClientConfiguration( + newProjectVersions = extractProjectVersions(text, "new.versions"), + existingProjectVersions = extractProjectVersions(text, "existing.versions"), + git = extractGitConfiguration(text) + ) - var currentVersionsMap: MutableMap? = null + private fun extractProjectVersions( + text: String, + versionLabel: String + ): Map { + val currentVersionsMap = mutableMapOf() + var isUnderLabel = false text.lineSequence().forEach { rawLine -> val line = rawLine.trim() if (line.isEmpty() || line.startsWith("#") || line.startsWith(";")) return@forEach if (line.startsWith("[") && line.endsWith("]")) { - currentVersionsMap = - when (line.substring(1, line.length - 1).lowercase()) { - "new.versions" -> newProjectVersions - "existing.versions" -> existingProjectVersions - else -> null - } + isUnderLabel = line.substring(1, line.length - 1).equals(versionLabel, true) return@forEach } - val index = line.indexOf('=') - if (index > 0 && currentVersionsMap != null) { - val key = line.take(index).trim() - val value = line.substring(index + 1).trim() - if (key.isNotEmpty() && value.isNotEmpty()) { - currentVersionsMap[key] = value + if (isUnderLabel) { + val index = line.indexOf('=') + if (index > 0) { + val key = line.take(index).trim() + val value = line.substring(index + 1).trim() + if (key.isNotEmpty() && value.isNotEmpty()) { + currentVersionsMap[key] = value + } } } } - return ClientConfiguration(newProjectVersions = newProjectVersions, existingProjectVersions = existingProjectVersions) + return currentVersionsMap + } + + private fun extractGitConfiguration(text: String): GitConfiguration { + var autoInitializeGit: Boolean? = null + var autoStageGit: Boolean? = null + text.lineSequence().forEach { rawLine -> + val line = rawLine.trim() + if ( + line.startsWith("[") && line.endsWith("]") && + line.substring(1, line.length - 1).equals("git", ignoreCase = true) + ) { + return@forEach + } + + if (line.startsWith("autoInitialize")) { + autoInitializeGit = line.substringAfter('=', "false").trim().toBoolean() + } else if (line.startsWith("autoStage")) { + autoStageGit = line.substringAfter('=', "true").trim().toBoolean() + } + } + return GitConfiguration(autoInitialize = autoInitializeGit, autoStage = autoStageGit) } } diff --git a/cli/src/main/kotlin/com/mitteloupe/cag/cli/flag/PrimaryFlag.kt b/cli/src/main/kotlin/com/mitteloupe/cag/cli/flag/PrimaryFlag.kt index cbf3f5d..ea06fbf 100644 --- a/cli/src/main/kotlin/com/mitteloupe/cag/cli/flag/PrimaryFlag.kt +++ b/cli/src/main/kotlin/com/mitteloupe/cag/cli/flag/PrimaryFlag.kt @@ -14,6 +14,19 @@ object SecondaryFlagConstants { const val OUTPUT_TYPE = "--output-type" const val HELP_TOPIC = "--topic" const val HELP_FORMAT = "--format" + const val GIT = "--git" +} + +object SecondaryFlags { + val ktlint = SecondaryFlag(long = SecondaryFlagConstants.KTLINT, short = "-kl", isBoolean = true) + val detekt = SecondaryFlag(long = SecondaryFlagConstants.DETEKT, short = "-d", isBoolean = true) + val git = + SecondaryFlag( + long = SecondaryFlagConstants.GIT, + short = "-g", + isBoolean = true, + missingErrorMessage = "Git flag must be used without a value" + ) } interface PrimaryFlag { @@ -34,10 +47,11 @@ interface PrimaryFlag { ), SecondaryFlag(SecondaryFlagConstants.PACKAGE, "-p"), SecondaryFlag(SecondaryFlagConstants.NO_COMPOSE, "-nc", isBoolean = true), - SecondaryFlag(SecondaryFlagConstants.KTLINT, "-kl", isBoolean = true), - SecondaryFlag(SecondaryFlagConstants.DETEKT, "-d", isBoolean = true), + SecondaryFlags.ktlint, + SecondaryFlags.detekt, SecondaryFlag(SecondaryFlagConstants.KTOR, "-kt", isBoolean = true), - SecondaryFlag(SecondaryFlagConstants.RETROFIT, "-rt", isBoolean = true) + SecondaryFlag(SecondaryFlagConstants.RETROFIT, "-rt", isBoolean = true), + SecondaryFlags.git ) } @@ -47,8 +61,9 @@ interface PrimaryFlag { override val secondaryFlags = listOf( SecondaryFlag(SecondaryFlagConstants.NO_COMPOSE, "-nc", isBoolean = true), - SecondaryFlag(SecondaryFlagConstants.KTLINT, "-kl", isBoolean = true), - SecondaryFlag(SecondaryFlagConstants.DETEKT, "-d", isBoolean = true) + SecondaryFlags.ktlint, + SecondaryFlags.detekt, + SecondaryFlags.git ) } @@ -64,8 +79,9 @@ interface PrimaryFlag { missingErrorMessage = "Feature name is required. Use --name=FeatureName or -n=FeatureName" ), SecondaryFlag(SecondaryFlagConstants.PACKAGE, "-p"), - SecondaryFlag(SecondaryFlagConstants.KTLINT, "-kl", isBoolean = true), - SecondaryFlag(SecondaryFlagConstants.DETEKT, "-d", isBoolean = true) + SecondaryFlags.ktlint, + SecondaryFlags.detekt, + SecondaryFlags.git ) } @@ -80,7 +96,8 @@ interface PrimaryFlag { isMandatory = true, missingErrorMessage = "Data source name is required. Use --name=DataSourceName or -n=DataSourceName" ), - SecondaryFlag(SecondaryFlagConstants.WITH, "-w") + SecondaryFlag(SecondaryFlagConstants.WITH, "-w"), + SecondaryFlags.git ) } @@ -112,7 +129,8 @@ interface PrimaryFlag { isMandatory = true, missingErrorMessage = "ViewModel name is required. Use ${SecondaryFlagConstants.NAME}=ViewModelName or -n=ViewModelName" ), - SecondaryFlag(SecondaryFlagConstants.PATH, "-p") + SecondaryFlag(SecondaryFlagConstants.PATH, "-p"), + SecondaryFlags.git ) } diff --git a/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/ArchitectureRequest.kt b/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/ArchitectureRequest.kt index 9cd502c..912855d 100644 --- a/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/ArchitectureRequest.kt +++ b/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/ArchitectureRequest.kt @@ -3,5 +3,6 @@ package com.mitteloupe.cag.cli.request data class ArchitectureRequest( val enableCompose: Boolean = true, val enableKtlint: Boolean = false, - val enableDetekt: Boolean = false + val enableDetekt: Boolean = false, + val enableGit: Boolean = false ) diff --git a/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/DataSourceRequest.kt b/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/DataSourceRequest.kt index 969c8ee..9dfd07f 100644 --- a/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/DataSourceRequest.kt +++ b/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/DataSourceRequest.kt @@ -3,5 +3,6 @@ package com.mitteloupe.cag.cli.request data class DataSourceRequest( val dataSourceName: String, val useKtor: Boolean, - val useRetrofit: Boolean + val useRetrofit: Boolean, + val enableGit: Boolean = false ) diff --git a/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/FeatureRequest.kt b/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/FeatureRequest.kt index 15087ad..cd939ba 100644 --- a/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/FeatureRequest.kt +++ b/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/FeatureRequest.kt @@ -4,5 +4,6 @@ data class FeatureRequest( val featureName: String, val packageName: String?, val enableKtlint: Boolean, - val enableDetekt: Boolean + val enableDetekt: Boolean, + val enableGit: Boolean = false ) diff --git a/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/ProjectTemplateRequest.kt b/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/ProjectTemplateRequest.kt index 293ee6f..ec0cae0 100644 --- a/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/ProjectTemplateRequest.kt +++ b/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/ProjectTemplateRequest.kt @@ -7,5 +7,6 @@ data class ProjectTemplateRequest( val enableKtlint: Boolean = false, val enableDetekt: Boolean = false, val enableKtor: Boolean = false, - val enableRetrofit: Boolean = false + val enableRetrofit: Boolean = false, + val enableGit: Boolean = false ) diff --git a/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/ViewModelRequest.kt b/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/ViewModelRequest.kt index dc836a9..784ef65 100644 --- a/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/ViewModelRequest.kt +++ b/cli/src/main/kotlin/com/mitteloupe/cag/cli/request/ViewModelRequest.kt @@ -2,5 +2,6 @@ package com.mitteloupe.cag.cli.request data class ViewModelRequest( val viewModelName: String, - val targetPath: String? + val targetPath: String?, + val enableGit: Boolean = false ) diff --git a/cli/src/test/kotlin/com/mitteloupe/cag/cli/AppArgumentProcessorTest.kt b/cli/src/test/kotlin/com/mitteloupe/cag/cli/AppArgumentProcessorTest.kt index 207769c..607463a 100644 --- a/cli/src/test/kotlin/com/mitteloupe/cag/cli/AppArgumentProcessorTest.kt +++ b/cli/src/test/kotlin/com/mitteloupe/cag/cli/AppArgumentProcessorTest.kt @@ -6,6 +6,8 @@ import com.mitteloupe.cag.cli.request.ProjectTemplateRequest import com.mitteloupe.cag.cli.request.UseCaseRequest import com.mitteloupe.cag.cli.request.ViewModelRequest import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Before import org.junit.Test @@ -246,6 +248,26 @@ class AppArgumentProcessorTest { assertEquals("Cannot mix short form (-nf) with long form secondary flags (--name). Use -n instead.", exception.message) } } + + @Test + fun `Given --new-feature with --git when getNewFeatures then returns FeatureRequest with git enabled`() { + // Given + val givenArguments = arrayOf("--new-feature", "--name=Quality", "--git") + val expectedRequest = + FeatureRequest( + featureName = "Quality", + packageName = null, + enableKtlint = false, + enableDetekt = false, + enableGit = true + ) + + // When + val result = classUnderTest.getNewFeatures(givenArguments) + + // Then + assertEquals(listOf(expectedRequest), result) + } } class AppArgumentProcessorArchitectureTest { @@ -350,9 +372,9 @@ class AppArgumentProcessorTest { // Then assertEquals(1, result.size) - assertEquals(true, result[0].enableCompose) - assertEquals(true, result[0].enableKtlint) - assertEquals(false, result[0].enableDetekt) + assertTrue(result[0].enableCompose) + assertTrue(result[0].enableKtlint) + assertFalse(result[0].enableDetekt) } @Test @@ -384,6 +406,19 @@ class AppArgumentProcessorTest { assertEquals(true, result[0].enableKtlint) assertEquals(false, result[0].enableDetekt) } + + @Test + fun `Given --new-architecture --git when getNewArchitecture then enableGit is true`() { + // Given + val givenArguments = arrayOf("--new-architecture", "--git") + + // When + val result = classUnderTest.getNewArchitecture(givenArguments) + + // Then + assertEquals(1, result.size) + assertTrue(result[0].enableGit) + } } class AppArgumentProcessorDataSourcesTest { @@ -530,6 +565,19 @@ class AppArgumentProcessorTest { ) } } + + @Test + fun `Given --git when getNewDataSources then enableGit is true`() { + // Given + val givenArguments = arrayOf("--new-datasource", "--name=My", "--git") + val expected = DataSourceRequest("MyDataSource", useKtor = false, useRetrofit = false, enableGit = true) + + // When + val result = classUnderTest.getNewDataSources(givenArguments) + + // Then + assertEquals(listOf(expected), result) + } } class AppArgumentProcessorUseCasesTest { @@ -911,6 +959,29 @@ class AppArgumentProcessorTest { assertEquals("Cannot mix short form (-np) with long form secondary flags (--name). Use -n instead.", exception.message) } } + + @Test + fun `Given project template with --git when getNewProjectTemplate then enableGit is true`() { + // Given + val givenArguments = arrayOf("--new-project", "--name=GitApp", "--git") + val expectedRequest = + ProjectTemplateRequest( + projectName = "GitApp", + packageName = "", + enableCompose = true, + enableKtlint = false, + enableDetekt = false, + enableKtor = false, + enableRetrofit = false, + enableGit = true + ) + + // When + val result = classUnderTest.getNewProjectTemplate(givenArguments) + + // Then + assertEquals(listOf(expectedRequest), result) + } } class AppArgumentProcessorViewModelsTest { @@ -992,6 +1063,19 @@ class AppArgumentProcessorTest { assertEquals("Cannot mix short form (-nvm) with long form secondary flags (--name). Use -n instead.", exception.message) } } + + @Test + fun `Given view model with --git when getNewViewModels then enableGit is true`() { + // Given + val givenArguments = arrayOf("--new-view-model", "--name=MyViewModel", "--git") + val expected = ViewModelRequest("MyViewModel", null, enableGit = true) + + // When + val result = classUnderTest.getNewViewModels(givenArguments) + + // Then + assertEquals(listOf(expected), result) + } } class UnknownFlagsValidation { diff --git a/cli/src/test/kotlin/com/mitteloupe/cag/cli/MainTest.kt b/cli/src/test/kotlin/com/mitteloupe/cag/cli/MainTest.kt index 417f1e8..5841144 100644 --- a/cli/src/test/kotlin/com/mitteloupe/cag/cli/MainTest.kt +++ b/cli/src/test/kotlin/com/mitteloupe/cag/cli/MainTest.kt @@ -15,13 +15,20 @@ import java.io.OutputStream import java.io.PrintStream import java.nio.file.Files +private const val NEW_PROJECT_SYNTAX = + "[--new-project --name=ProjectName --package=PackageName [--no-compose] [--ktlint] [--detekt] [--ktor] [--retrofit] [--git]]" +private const val NEW_ARCHITECTURE_SYNTAX = "[--new-architecture [--no-compose] [--ktlint] [--detekt] [--git]]" +private const val NEW_FEATURE_SYNTAX = "[--new-feature --name=FeatureName [--package=PackageName] [--ktlint] [--detekt] [--git]]" +private const val NEW_DATASOURCE_SYNTAX = "[--new-datasource --name=DataSourceName [--with=ktor|retrofit|ktor,retrofit] [--git]]" +private const val NEW_USE_CASE_SYNTAX = "[--new-use-case --name=UseCaseName [--path=TargetPath] [--git]]" +private const val NEW_VIEW_MODEL_SYNTAX = "[--new-view-model --name=ViewModelName [--path=TargetPath] [--git]]" private const val SHORT_USAGE = - "usage: cag [--new-project --name=ProjectName --package=PackageName [--no-compose] [--ktlint] [--detekt] [--ktor] [--retrofit]]... " + - "[--new-architecture [--no-compose] [--ktlint] [--detekt]]... " + - "[--new-feature --name=FeatureName [--package=PackageName] [--ktlint] [--detekt]]... " + - "[--new-datasource --name=DataSourceName [--with=ktor|retrofit|ktor,retrofit]]... " + - "[--new-use-case --name=UseCaseName [--path=TargetPath]]... " + - "[--new-view-model --name=ViewModelName [--path=TargetPath]]..." + "usage: cag $NEW_PROJECT_SYNTAX... " + + "$NEW_ARCHITECTURE_SYNTAX... " + + "$NEW_FEATURE_SYNTAX... " + + "$NEW_DATASOURCE_SYNTAX... " + + "$NEW_USE_CASE_SYNTAX... " + + "$NEW_VIEW_MODEL_SYNTAX..." @RunWith(Enclosed::class) @SuiteClasses( diff --git a/cli/src/test/kotlin/com/mitteloupe/cag/cli/configuration/VersionConfigLoaderTest.kt b/cli/src/test/kotlin/com/mitteloupe/cag/cli/configuration/VersionConfigLoaderTest.kt index a542948..5de36cf 100644 --- a/cli/src/test/kotlin/com/mitteloupe/cag/cli/configuration/VersionConfigLoaderTest.kt +++ b/cli/src/test/kotlin/com/mitteloupe/cag/cli/configuration/VersionConfigLoaderTest.kt @@ -1,6 +1,8 @@ package com.mitteloupe.cag.cli.configuration import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import java.io.File @@ -49,6 +51,24 @@ class VersionConfigLoaderTest { assertEquals(expectedClientConfiguration, actualConfiguration) } + @Test + fun `Given INI with git section when parse then parses git booleans`() { + // Given + val text = + """ + [git] + autoInitialize=true + autoStage=false + """.trimIndent() + + // When + val actualConfiguration = classUnderTest.parse(text) + + // Then + assertTrue(actualConfiguration.git.autoInitialize!!) + assertFalse(actualConfiguration.git.autoStage!!) + } + @Test fun `Given home, project configurations when loadFromFiles then project overrides home`() { // Given @@ -63,6 +83,9 @@ class VersionConfigLoaderTest { ktor=1.0.0 [existing.versions] retrofit=2.0.0 + [git] + autoInitialize=true + autoStage=false """.trimIndent() ) } @@ -76,6 +99,9 @@ class VersionConfigLoaderTest { [existing.versions] retrofit=2.11.0 okhttp3=4.12.0 + [git] + autoInitialize=false + autoStage=true """.trimIndent() ) } @@ -90,7 +116,8 @@ class VersionConfigLoaderTest { mapOf( "retrofit" to "2.11.0", "okhttp3" to "4.12.0" - ) + ), + git = GitConfiguration(autoInitialize = false, autoStage = true) ) // When diff --git a/git/build.gradle.kts b/git/build.gradle.kts new file mode 100644 index 0000000..f75c494 --- /dev/null +++ b/git/build.gradle.kts @@ -0,0 +1,38 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlin.jvm) + id("java-library") + alias(libs.plugins.ktlint) +} + +repositories { + mavenCentral() +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } +} + +dependencies { + implementation(kotlin("stdlib")) + testImplementation(libs.junit4) + testImplementation(libs.mockk) +} + +ktlint { + android.set(false) + ignoreFailures.set(false) + filter { + exclude("**/build/**") + include("**/*.kt") + include("**/*.kts") + } +} diff --git a/git/src/main/kotlin/com/mitteloupe/cag/git/Git.kt b/git/src/main/kotlin/com/mitteloupe/cag/git/Git.kt new file mode 100644 index 0000000..b1414ad --- /dev/null +++ b/git/src/main/kotlin/com/mitteloupe/cag/git/Git.kt @@ -0,0 +1,51 @@ +package com.mitteloupe.cag.git + +import java.io.File + +class Git( + private val processExecutor: ProcessExecutor = ProcessExecutor() +) { + fun initializeRepository(directory: File): Boolean { + if (isGitRepository(directory)) { + return false + } + + return processExecutor.run( + directory, + listOf("git", "init") + ) + } + + fun stage( + projectRoot: File, + files: Collection + ) { + if (files.isEmpty()) { + return + } + val gitCommandWithArguments = + listOf("git", "add", "--") + + files.map { file -> + val absolutePath = file.absolutePath + val file = File(absolutePath) + file.relativeToOrNull(projectRoot)?.path ?: absolutePath + } + processExecutor.run(projectRoot, gitCommandWithArguments) + } + + fun stageAll(directory: File): Boolean { + if (!isGitRepository(directory)) { + return false + } + + return processExecutor.run( + directory, + listOf("git", "add", "-A") + ) + } + + fun isGitRepository(directory: File): Boolean { + val gitDirectory = File(directory, ".git") + return gitDirectory.exists() && gitDirectory.isDirectory + } +} diff --git a/git/src/main/kotlin/com/mitteloupe/cag/git/ProcessExecutor.kt b/git/src/main/kotlin/com/mitteloupe/cag/git/ProcessExecutor.kt new file mode 100644 index 0000000..1265a5b --- /dev/null +++ b/git/src/main/kotlin/com/mitteloupe/cag/git/ProcessExecutor.kt @@ -0,0 +1,32 @@ +package com.mitteloupe.cag.git + +import java.io.File + +class ProcessExecutor { + fun run( + workingDirectory: File, + command: List + ): Boolean = + try { + val process = + ProcessBuilder(command) + .directory(workingDirectory) + .redirectErrorStream(true) + .start() + val output = process.inputStream.bufferedReader().use { it.readText() } + val exitCode = process.waitFor() + if (exitCode == 0) { + true + } else { + println( + "Command failed: '${command.joinToString(" ")}' in '${workingDirectory.absolutePath}' (exit=$exitCode)\n" + + "Output:\n$output" + ) + false + } + } catch (exception: Exception) { + println("Process execution failed for '${command.joinToString(" ")}' in '${workingDirectory.absolutePath}'") + println(exception.stackTraceToString()) + false + } +} diff --git a/plugin/src/test/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/git/GitStagerTest.kt b/git/src/test/kotlin/com/mitteloupe/cag/git/GitTest.kt similarity index 92% rename from plugin/src/test/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/git/GitStagerTest.kt rename to git/src/test/kotlin/com/mitteloupe/cag/git/GitTest.kt index 07975cd..145e434 100644 --- a/plugin/src/test/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/git/GitStagerTest.kt +++ b/git/src/test/kotlin/com/mitteloupe/cag/git/GitTest.kt @@ -1,4 +1,4 @@ -package com.mitteloupe.cag.cleanarchitecturegenerator.git +package com.mitteloupe.cag.git import io.mockk.MockKAnnotations import io.mockk.every @@ -10,8 +10,8 @@ import org.junit.Test import java.io.File import kotlin.io.path.createTempDirectory -class GitStagerTest { - private lateinit var classUnderTest: GitStager +class GitTest { + private lateinit var classUnderTest: Git private lateinit var projectRoot: File @@ -22,7 +22,7 @@ class GitStagerTest { fun setUp() { MockKAnnotations.init(this) projectRoot = createTempDirectory(prefix = "projRoot_").toFile() - classUnderTest = GitStager(executor) + classUnderTest = Git(executor) } @After @@ -41,7 +41,7 @@ class GitStagerTest { val file2 = File(projectRoot, filename2).apply { writeText("b") } val givenFiles = listOf(file1, file2) val expectedArguments = listOf("git", "add", "--", "$relativePath/$filename1", filename2) - every { executor.run(projectRoot, expectedArguments) } returns Unit + every { executor.run(projectRoot, expectedArguments) } returns true // When classUnderTest.stage(projectRoot, givenFiles) @@ -67,7 +67,7 @@ class GitStagerTest { val externalFile = File(externalDirectory, filename).apply { writeText("c") } val givenFiles = listOf(externalFile) val expectedArguments = listOf("git", "add", "--", "../${externalDirectory.name}/$filename") - every { executor.run(projectRoot, expectedArguments) } returns Unit + every { executor.run(projectRoot, expectedArguments) } returns true try { // When diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index b47c5aa..21c8e66 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -25,6 +25,7 @@ intellijPlatform { dependencies { implementation(project(":core")) + implementation(project(":git")) intellijPlatform { androidStudio(libs.versions.androidStudio.get()) diff --git a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/git/GitAddQueueService.kt b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/git/GitAddQueueService.kt index 3d04256..a9d85af 100644 --- a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/git/GitAddQueueService.kt +++ b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/git/GitAddQueueService.kt @@ -2,13 +2,14 @@ package com.mitteloupe.cag.cleanarchitecturegenerator.git import com.intellij.openapi.components.Service import com.intellij.openapi.project.Project +import com.mitteloupe.cag.git.Git import java.io.File import java.util.concurrent.ConcurrentSkipListSet @Service(Service.Level.PROJECT) class GitAddQueueService(private val project: Project) { private val queue = ConcurrentSkipListSet() - private val gitStager = GitStager() + private val git = Git() fun enqueue(file: File) { val path = file.absolutePath @@ -27,7 +28,7 @@ class GitAddQueueService(private val project: Project) { return } - gitStager.stage(projectRoot, items.map(::File)) + git.stage(projectRoot, items.map(::File)) queue.removeAll(items) } } diff --git a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/git/GitInitializer.kt b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/git/GitInitializer.kt deleted file mode 100644 index dbde39d..0000000 --- a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/git/GitInitializer.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.mitteloupe.cag.cleanarchitecturegenerator.git - -import java.io.File - -internal class GitInitializer(private val executor: ProcessExecutor = ProcessExecutor()) { - fun initialize(projectRoot: File) { - executor.run(projectRoot, listOf("git", "init")) - } -} diff --git a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/git/GitStager.kt b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/git/GitStager.kt deleted file mode 100644 index 6a1ec90..0000000 --- a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/git/GitStager.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.mitteloupe.cag.cleanarchitecturegenerator.git - -import java.io.File - -internal class GitStager(private val executor: ProcessExecutor = ProcessExecutor()) { - fun stage( - projectRoot: File, - files: Collection - ) { - if (files.isEmpty()) { - return - } - val gitCommandWithArguments = - listOf("git", "add", "--") + - files.map { file -> - val absolutePath = file.absolutePath - val file = File(absolutePath) - file.relativeToOrNull(projectRoot)?.path ?: absolutePath - } - executor.run(projectRoot, gitCommandWithArguments) - } - - fun stageAll(projectRoot: File) { - executor.run(projectRoot, listOf("git", "add", "-A")) - } -} diff --git a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/git/ProcessExecutor.kt b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/git/ProcessExecutor.kt deleted file mode 100644 index 0aae9b7..0000000 --- a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/git/ProcessExecutor.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.mitteloupe.cag.cleanarchitecturegenerator.git - -import com.intellij.openapi.diagnostic.Logger -import java.io.File - -internal class ProcessExecutor { - private val logger = Logger.getInstance(ProcessExecutor::class.java) - - fun run( - directory: File, - args: List - ) { - try { - val process = - ProcessBuilder(args) - .directory(directory) - .redirectErrorStream(true) - .start() - val output = process.inputStream.bufferedReader().use { it.readText() } - val exitCode = process.waitFor() - if (exitCode != 0) { - logger.warn("Command failed: '${args.joinToString(" ")}' in '${directory.absolutePath}' (exit=$exitCode)\nOutput:\n$output") - } - } catch (e: Exception) { - logger.warn("Process execution failed for '${args.joinToString(" ")}' in '${directory.absolutePath}'", e) - } - } -} diff --git a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/projectwizard/CleanArchitectureWizardTemplateProvider.kt b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/projectwizard/CleanArchitectureWizardTemplateProvider.kt index 4f9b9c4..883ebaf 100644 --- a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/projectwizard/CleanArchitectureWizardTemplateProvider.kt +++ b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/projectwizard/CleanArchitectureWizardTemplateProvider.kt @@ -18,17 +18,17 @@ import com.android.tools.idea.wizard.template.template import com.mitteloupe.cag.cleanarchitecturegenerator.CleanArchitectureGeneratorBundle import com.mitteloupe.cag.cleanarchitecturegenerator.IdeBridge import com.mitteloupe.cag.cleanarchitecturegenerator.filesystem.GeneratorProvider -import com.mitteloupe.cag.cleanarchitecturegenerator.git.GitInitializer -import com.mitteloupe.cag.cleanarchitecturegenerator.git.GitStager import com.mitteloupe.cag.cleanarchitecturegenerator.settings.AppSettingsService import com.mitteloupe.cag.core.GenerationException import com.mitteloupe.cag.core.request.GenerateProjectTemplateRequest +import com.mitteloupe.cag.git.Git import java.io.File import java.lang.reflect.Field class CleanArchitectureWizardTemplateProvider : WizardTemplateProvider() { private val ideBridge = IdeBridge() private val generatorProvider = GeneratorProvider() + private val git = Git() override fun getTemplates(): List