Auto-commit after successful build (2026-04-21 21:22:08)
This commit is contained in:
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
.gradle/
|
||||
build/
|
||||
app/build/
|
||||
local.properties
|
||||
.idea/
|
||||
android-sdk/
|
||||
jdk/
|
||||
.tmp-android-build/
|
||||
.setup-build-environment.state
|
||||
78
README.md
Normal file
78
README.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# GitHug Android
|
||||
|
||||
Native Android adaptation of the GitHug learning game, built with **Kotlin + Jetpack Compose** and designed for **mobile-first usability** while preserving a **CLI-first play style**.
|
||||
|
||||
## Current MVP scope
|
||||
|
||||
This repository contains a starter Android app with:
|
||||
|
||||
- Kotlin + Jetpack Compose project scaffolding
|
||||
- CLI-first gameplay shell
|
||||
- Optional hideable visualization panel
|
||||
- In-memory Git sandbox engine for early commands
|
||||
- First playable GitHug-inspired levels: `init`, `add`, and `commit`
|
||||
|
||||
## Product direction
|
||||
|
||||
The app supports three modes:
|
||||
|
||||
- **CLI ONLY**: hide all visualization and play with terminal-style input only
|
||||
- **HYBRID**: command line with optional repo panels
|
||||
- **VISUAL**: repo panels visible by default, still backed by the same command engine
|
||||
|
||||
## Building
|
||||
|
||||
You will need a local Android SDK installation and either Android Studio or SDK command line tools.
|
||||
|
||||
Typical next step once the SDK is installed:
|
||||
|
||||
```bash
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
|
||||
## Automated environment setup
|
||||
|
||||
This repo includes a root-level setup script:
|
||||
|
||||
```bash
|
||||
bash ./SetupBuildEnvironment.sh
|
||||
```
|
||||
|
||||
It is designed to be idempotent and uses project-relative paths to:
|
||||
|
||||
- install a project-local JDK into `./jdk`
|
||||
- install Android command-line tools into `./android-sdk`
|
||||
- restore `gradle/wrapper/gradle-wrapper.jar` if missing
|
||||
- install required Android SDK packages
|
||||
- write `local.properties`
|
||||
|
||||
To also build the debug APK without leaving Gradle running in the background:
|
||||
|
||||
```bash
|
||||
bash ./SetupBuildEnvironment.sh --build
|
||||
```
|
||||
|
||||
The script and project are configured to prefer non-daemon Gradle usage.
|
||||
|
||||
It also avoids depending on the system Java version by provisioning a project-local **JDK 17**, which is required by current Android SDK command-line tools.
|
||||
|
||||
To avoid repeated expensive SDK verification, the script writes a small state file named:
|
||||
|
||||
`./.setup-build-environment.state`
|
||||
|
||||
If all required components were verified successfully, the script will skip SDK verification/reinstallation for the next **24 hours** unless required directories are missing.
|
||||
|
||||
When you run:
|
||||
|
||||
```bash
|
||||
bash ./SetupBuildEnvironment.sh --build
|
||||
```
|
||||
|
||||
the script will also attempt to create a **git commit after a successful build** if there are source changes to commit.
|
||||
|
||||
## Next steps toward full GitHug parity
|
||||
|
||||
- Expand the Git sandbox to cover merge, rebase, stash, tags, remotes, reset, revert, cherry-pick, bisect, and more
|
||||
- Port all original levels and hints into structured content files
|
||||
- Add richer validation rules and per-level explanations
|
||||
- Add onboarding, accessibility polish, icons, tests, and Play Store assets
|
||||
255
SetupBuildEnvironment.sh
Executable file
255
SetupBuildEnvironment.sh
Executable file
@@ -0,0 +1,255 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$SCRIPT_DIR"
|
||||
TMP_DIR="$PROJECT_DIR/.tmp-android-build"
|
||||
SDK_DIR="$PROJECT_DIR/android-sdk"
|
||||
JDK_DIR="$PROJECT_DIR/jdk"
|
||||
STATE_FILE="$PROJECT_DIR/.setup-build-environment.state"
|
||||
CMDLINE_TOOLS_DIR="$SDK_DIR/cmdline-tools"
|
||||
CMDLINE_TOOLS_LATEST_DIR="$CMDLINE_TOOLS_DIR/latest"
|
||||
WRAPPER_JAR_PATH="$PROJECT_DIR/gradle/wrapper/gradle-wrapper.jar"
|
||||
LOCAL_PROPERTIES_PATH="$PROJECT_DIR/local.properties"
|
||||
|
||||
ANDROID_CMDLINE_TOOLS_URL="https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip"
|
||||
GRADLE_DIST_URL="https://services.gradle.org/distributions/gradle-8.7-bin.zip"
|
||||
JDK_DIST_URL="https://api.adoptium.net/v3/binary/latest/17/ga/linux/x64/jdk/hotspot/normal/eclipse"
|
||||
STATE_TTL_SECONDS=86400
|
||||
|
||||
log() {
|
||||
printf '\n[%s] %s\n' "setup" "$1"
|
||||
}
|
||||
|
||||
require_tool() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Missing required tool: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_dir() {
|
||||
mkdir -p "$1"
|
||||
}
|
||||
|
||||
current_epoch() {
|
||||
date +%s
|
||||
}
|
||||
|
||||
have_recent_success_marker() {
|
||||
if [ ! -f "$STATE_FILE" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local recorded_epoch
|
||||
recorded_epoch="$(cat "$STATE_FILE" 2>/dev/null || true)"
|
||||
if ! printf '%s' "$recorded_epoch" | grep -Eq '^[0-9]+$'; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local now
|
||||
now="$(current_epoch)"
|
||||
[ $((now - recorded_epoch)) -lt "$STATE_TTL_SECONDS" ]
|
||||
}
|
||||
|
||||
mark_successful_check() {
|
||||
current_epoch > "$STATE_FILE"
|
||||
}
|
||||
|
||||
auto_commit_if_needed() {
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
log "Git is not available; skipping automatic commit"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! git -C "$PROJECT_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
log "Project is not inside a git work tree; skipping automatic commit"
|
||||
return
|
||||
fi
|
||||
|
||||
git -C "$PROJECT_DIR" add -A
|
||||
|
||||
if git -C "$PROJECT_DIR" diff --cached --quiet; then
|
||||
log "No source changes to commit after successful build"
|
||||
return
|
||||
fi
|
||||
|
||||
local commit_message
|
||||
commit_message="Auto-commit after successful build ($(date '+%Y-%m-%d %H:%M:%S'))"
|
||||
log "Creating git commit for the latest successful build"
|
||||
git -C "$PROJECT_DIR" commit -m "$commit_message"
|
||||
}
|
||||
|
||||
download_if_missing() {
|
||||
local url="$1"
|
||||
local output="$2"
|
||||
if [ -f "$output" ]; then
|
||||
log "Using existing download: ${output#$PROJECT_DIR/}"
|
||||
else
|
||||
log "Downloading $(basename "$output")"
|
||||
curl -L "$url" -o "$output"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_cmdline_tools() {
|
||||
if [ -x "$CMDLINE_TOOLS_LATEST_DIR/bin/sdkmanager" ]; then
|
||||
log "Android command-line tools already present"
|
||||
return
|
||||
fi
|
||||
|
||||
ensure_dir "$CMDLINE_TOOLS_DIR"
|
||||
ensure_dir "$TMP_DIR"
|
||||
local zip_path="$TMP_DIR/commandlinetools.zip"
|
||||
download_if_missing "$ANDROID_CMDLINE_TOOLS_URL" "$zip_path"
|
||||
|
||||
log "Extracting Android command-line tools"
|
||||
rm -rf "$CMDLINE_TOOLS_LATEST_DIR"
|
||||
unzip -q -o "$zip_path" -d "$CMDLINE_TOOLS_DIR"
|
||||
if [ -d "$CMDLINE_TOOLS_DIR/cmdline-tools" ]; then
|
||||
mv "$CMDLINE_TOOLS_DIR/cmdline-tools" "$CMDLINE_TOOLS_LATEST_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_wrapper_jar() {
|
||||
if [ -f "$WRAPPER_JAR_PATH" ]; then
|
||||
log "Gradle wrapper jar already present"
|
||||
return
|
||||
fi
|
||||
|
||||
ensure_dir "$TMP_DIR"
|
||||
local zip_path="$TMP_DIR/gradle-8.7-bin.zip"
|
||||
local gradle_extract_dir="$TMP_DIR/gradle-8.7"
|
||||
local plugin_wrapper_jar="$gradle_extract_dir/lib/plugins/gradle-wrapper-8.7.jar"
|
||||
download_if_missing "$GRADLE_DIST_URL" "$zip_path"
|
||||
|
||||
log "Extracting Gradle wrapper jar"
|
||||
rm -rf "$gradle_extract_dir"
|
||||
unzip -q -o "$zip_path" -d "$TMP_DIR"
|
||||
python - <<PY
|
||||
import zipfile
|
||||
src = r'''$plugin_wrapper_jar'''
|
||||
dst = r'''$WRAPPER_JAR_PATH'''
|
||||
with zipfile.ZipFile(src) as zf:
|
||||
data = zf.read('gradle-wrapper.jar')
|
||||
with open(dst, 'wb') as f:
|
||||
f.write(data)
|
||||
PY
|
||||
}
|
||||
|
||||
ensure_jdk17() {
|
||||
if [ -x "$JDK_DIR/bin/java" ]; then
|
||||
log "Project-local JDK already present"
|
||||
return
|
||||
fi
|
||||
|
||||
ensure_dir "$TMP_DIR"
|
||||
local archive_path="$TMP_DIR/jdk17.tar.gz"
|
||||
local extract_dir="$TMP_DIR/jdk17-extract"
|
||||
rm -f "$archive_path"
|
||||
download_if_missing "$JDK_DIST_URL" "$archive_path"
|
||||
|
||||
log "Extracting project-local JDK"
|
||||
rm -rf "$extract_dir" "$JDK_DIR"
|
||||
mkdir -p "$extract_dir"
|
||||
if ! file "$archive_path" | grep -qiE 'gzip compressed|tar archive'; then
|
||||
echo "Downloaded JDK archive is not a valid tar.gz: $archive_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
tar -xzf "$archive_path" -C "$extract_dir"
|
||||
local extracted_root
|
||||
extracted_root="$(find "$extract_dir" -mindepth 1 -maxdepth 1 -type d | head -n 1)"
|
||||
if [ -z "$extracted_root" ]; then
|
||||
echo "Unable to locate extracted JDK directory" >&2
|
||||
exit 1
|
||||
fi
|
||||
mv "$extracted_root" "$JDK_DIR"
|
||||
}
|
||||
|
||||
setup_java_env() {
|
||||
export JAVA_HOME="$JDK_DIR"
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
}
|
||||
|
||||
write_local_properties() {
|
||||
log "Writing local.properties"
|
||||
cat > "$LOCAL_PROPERTIES_PATH" <<EOF_INNER
|
||||
sdk.dir=$SDK_DIR
|
||||
EOF_INNER
|
||||
}
|
||||
|
||||
setup_env() {
|
||||
setup_java_env
|
||||
export ANDROID_HOME="$SDK_DIR"
|
||||
export ANDROID_SDK_ROOT="$SDK_DIR"
|
||||
}
|
||||
|
||||
ensure_sdk_packages() {
|
||||
setup_env
|
||||
|
||||
if have_recent_success_marker \
|
||||
&& [ -d "$SDK_DIR/platform-tools" ] \
|
||||
&& [ -d "$SDK_DIR/platforms/android-34" ] \
|
||||
&& [ -d "$SDK_DIR/build-tools/34.0.0" ]; then
|
||||
log "Skipping SDK verification/install because all components were confirmed within the last 24 hours"
|
||||
return
|
||||
fi
|
||||
|
||||
log "Accepting Android SDK licenses"
|
||||
set +e
|
||||
set +o pipefail
|
||||
yes | "$CMDLINE_TOOLS_LATEST_DIR/bin/sdkmanager" --sdk_root="$SDK_DIR" --licenses >/dev/null
|
||||
local license_status=$?
|
||||
set -o pipefail
|
||||
set -e
|
||||
if [ "$license_status" -ne 0 ]; then
|
||||
echo "sdkmanager --licenses failed with exit code $license_status" >&2
|
||||
exit "$license_status"
|
||||
fi
|
||||
|
||||
log "Installing required Android SDK packages"
|
||||
"$CMDLINE_TOOLS_LATEST_DIR/bin/sdkmanager" --sdk_root="$SDK_DIR" \
|
||||
"platform-tools" \
|
||||
"platforms;android-34" \
|
||||
"build-tools;34.0.0"
|
||||
|
||||
mark_successful_check
|
||||
}
|
||||
|
||||
maybe_build() {
|
||||
if [ "${1:-}" != "--build" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
setup_env
|
||||
log "Building debug APK with --no-daemon"
|
||||
"$PROJECT_DIR/gradlew" --no-daemon assembleDebug
|
||||
auto_commit_if_needed
|
||||
log "Stopping any Gradle daemons just in case"
|
||||
"$PROJECT_DIR/gradlew" --stop >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
main() {
|
||||
require_tool curl
|
||||
require_tool unzip
|
||||
require_tool tar
|
||||
|
||||
ensure_dir "$TMP_DIR"
|
||||
ensure_dir "$SDK_DIR"
|
||||
|
||||
ensure_jdk17
|
||||
ensure_cmdline_tools
|
||||
ensure_wrapper_jar
|
||||
write_local_properties
|
||||
ensure_sdk_packages
|
||||
maybe_build "${1:-}"
|
||||
|
||||
log "Environment ready"
|
||||
log "Project dir: $PROJECT_DIR"
|
||||
log "JDK dir: $JDK_DIR"
|
||||
log "SDK dir: $SDK_DIR"
|
||||
log "To build without a background Gradle daemon: ./gradlew --no-daemon assembleDebug"
|
||||
log "To setup and build in one step: bash ./SetupBuildEnvironment.sh --build"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
78
app/build.gradle.kts
Normal file
78
app/build.gradle.kts
Normal file
@@ -0,0 +1,78 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.kawomi.githugandroid"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.kawomi.githugandroid"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "0.1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.14"
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val composeBom = platform("androidx.compose:compose-bom:2024.06.00")
|
||||
|
||||
implementation("androidx.core:core-ktx:1.13.1")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.3")
|
||||
implementation("androidx.activity:activity-compose:1.9.1")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.3")
|
||||
implementation("androidx.datastore:datastore-preferences:1.1.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
|
||||
|
||||
implementation(composeBom)
|
||||
androidTestImplementation(composeBom)
|
||||
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation("androidx.compose.ui:ui-graphics")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("androidx.compose.foundation:foundation")
|
||||
implementation("androidx.compose.material3:material3:1.2.1")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
|
||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||
}
|
||||
0
app/proguard-rules.pro
vendored
Normal file
0
app/proguard-rules.pro
vendored
Normal file
22
app/src/main/AndroidManifest.xml
Normal file
22
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@android:drawable/sym_def_app_icon"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@android:drawable/sym_def_app_icon"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.GithugAndroid">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
174
app/src/main/java/com/kawomi/githugandroid/GameModels.kt
Normal file
174
app/src/main/java/com/kawomi/githugandroid/GameModels.kt
Normal file
@@ -0,0 +1,174 @@
|
||||
package com.kawomi.githugandroid
|
||||
|
||||
import androidx.compose.runtime.saveable.listSaver
|
||||
|
||||
enum class PlayMode(val label: String) {
|
||||
CLI_ONLY("CLI ONLY"),
|
||||
VISUAL("VISUAL"),
|
||||
}
|
||||
|
||||
data class GitFile(
|
||||
val name: String,
|
||||
val content: String = "",
|
||||
val staged: Boolean = false,
|
||||
)
|
||||
|
||||
data class CommitNode(
|
||||
val id: String,
|
||||
val message: String,
|
||||
)
|
||||
|
||||
data class RepoState(
|
||||
val initialized: Boolean = false,
|
||||
val files: List<GitFile> = emptyList(),
|
||||
val commits: List<CommitNode> = emptyList(),
|
||||
val headBranch: String = "master",
|
||||
val branches: Map<String, Int> = emptyMap(),
|
||||
)
|
||||
|
||||
data class Level(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val hints: List<String>,
|
||||
val commandSuggestions: List<String>,
|
||||
val validator: (RepoState) -> Boolean,
|
||||
val setup: () -> RepoState,
|
||||
)
|
||||
|
||||
fun sampleLevels(): List<Level> = listOf(
|
||||
Level(
|
||||
id = "init",
|
||||
title = "Init",
|
||||
description = "A new directory, git_hug, has been created. Initialize an empty repository in it.",
|
||||
hints = listOf("Use git init to create a new repository.", "Try `git init` in the command area."),
|
||||
commandSuggestions = listOf("git init", "git status"),
|
||||
validator = { it.initialized },
|
||||
setup = { RepoState() },
|
||||
),
|
||||
Level(
|
||||
id = "add",
|
||||
title = "Add",
|
||||
description = "There is a file in your folder called README; add it to your staging area.",
|
||||
hints = listOf("You want to stage README.", "Use `git add README`."),
|
||||
commandSuggestions = listOf("git status", "git add README", "ls"),
|
||||
validator = { repo -> repo.files.any { it.name == "README" && it.staged } },
|
||||
setup = { RepoState(initialized = true, files = listOf(GitFile("README")), branches = mapOf("master" to 0)) },
|
||||
),
|
||||
Level(
|
||||
id = "commit",
|
||||
title = "Commit",
|
||||
description = "The README file has been added to your staging area, now commit it.",
|
||||
hints = listOf("You must include a message when you commit.", "Use `git commit -m \"message\"`."),
|
||||
commandSuggestions = listOf("git status", "git commit -m \"Initial commit\"", "git log"),
|
||||
validator = { repo -> repo.commits.isNotEmpty() },
|
||||
setup = { RepoState(initialized = true, files = listOf(GitFile("README", staged = true)), branches = mapOf("master" to 0)) },
|
||||
),
|
||||
)
|
||||
|
||||
val RepoStateSaver = listSaver<RepoState, Any>(
|
||||
save = { state ->
|
||||
listOf(
|
||||
state.initialized,
|
||||
state.headBranch,
|
||||
state.files.flatMap { listOf(it.name, it.content, it.staged.toString()) },
|
||||
state.commits.flatMap { listOf(it.id, it.message) },
|
||||
state.branches.flatMap { listOf(it.key, it.value.toString()) },
|
||||
)
|
||||
},
|
||||
restore = { saved ->
|
||||
val initialized = saved[0] as Boolean
|
||||
val headBranch = saved[1] as String
|
||||
val fileParts = saved[2] as List<*>
|
||||
val commitParts = saved[3] as List<*>
|
||||
val branchParts = saved[4] as List<*>
|
||||
RepoState(
|
||||
initialized = initialized,
|
||||
headBranch = headBranch,
|
||||
files = fileParts.chunked(3).map {
|
||||
GitFile(it[0] as String, it[1] as String, (it[2] as String).toBoolean())
|
||||
},
|
||||
commits = commitParts.chunked(2).map {
|
||||
CommitNode(it[0] as String, it[1] as String)
|
||||
},
|
||||
branches = branchParts.chunked(2).associate { (it[0] as String) to (it[1] as String).toInt() }
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
object GitSandboxEngine {
|
||||
fun execute(repo: RepoState, command: String): Pair<RepoState, List<String>> {
|
||||
val parts = command.split(" ").filter { it.isNotBlank() }
|
||||
if (parts.isEmpty()) return repo to emptyList()
|
||||
return when {
|
||||
parts[0] == "touch" && parts.size >= 2 -> {
|
||||
val name = parts[1]
|
||||
if (repo.files.any { it.name == name }) repo to listOf("$name already exists")
|
||||
else repo.copy(files = repo.files + GitFile(name = name)) to emptyList()
|
||||
}
|
||||
parts[0] == "ls" -> repo to repo.files.map { it.name }.ifEmpty { listOf() }
|
||||
parts[0] != "git" -> repo to listOf("Command not supported in sandbox. Try a git command or 'touch'.")
|
||||
parts.size >= 2 && parts[1] == "init" -> repo.copy(initialized = true, branches = mapOf("master" to repo.commits.size)) to listOf("Initialized empty Git repository")
|
||||
!repo.initialized -> repo to listOf("fatal: not a git repository")
|
||||
parts.size >= 2 && parts[1] == "status" -> repo to statusLines(repo)
|
||||
parts.size >= 3 && parts[1] == "add" -> {
|
||||
val target = parts[2]
|
||||
val updated = repo.files.map { if (target == "." || it.name == target) it.copy(staged = true) else it }
|
||||
repo.copy(files = updated) to listOf("staged ${if (target == ".") "all files" else target}")
|
||||
}
|
||||
parts.size >= 2 && parts[1] == "commit" -> commit(repo, parts)
|
||||
parts.size >= 2 && parts[1] == "log" -> {
|
||||
repo to if (repo.commits.isEmpty()) listOf("fatal: your current branch '${repo.headBranch}' does not have any commits yet")
|
||||
else repo.commits.reversed().flatMap { listOf("commit ${it.id}", " ${it.message}") }
|
||||
}
|
||||
parts.size >= 3 && parts[1] == "branch" -> {
|
||||
val branch = parts[2]
|
||||
if (repo.branches.containsKey(branch)) repo to listOf("fatal: a branch named '$branch' already exists")
|
||||
else repo.copy(branches = repo.branches + (branch to repo.commits.size)) to listOf("Created branch $branch")
|
||||
}
|
||||
parts.size >= 3 && parts[1] == "checkout" -> {
|
||||
val branch = parts[2]
|
||||
if (!repo.branches.containsKey(branch)) repo to listOf("error: pathspec '$branch' did not match any branch")
|
||||
else repo.copy(headBranch = branch) to listOf("Switched to branch '$branch'")
|
||||
}
|
||||
else -> repo to listOf("Unsupported git command in MVP sandbox: ${parts.drop(1).joinToString(" ")}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun commit(repo: RepoState, parts: List<String>): Pair<RepoState, List<String>> {
|
||||
val messageIndex = parts.indexOf("-m")
|
||||
if (messageIndex == -1 || messageIndex == parts.lastIndex) {
|
||||
return repo to listOf("error: commit message required. Use git commit -m \"message\"")
|
||||
}
|
||||
val message = parts.drop(messageIndex + 1).joinToString(" ").trim('"')
|
||||
val staged = repo.files.filter { it.staged }
|
||||
if (staged.isEmpty()) return repo to listOf("nothing to commit")
|
||||
val nextId = "${repo.commits.size + 1}".padStart(7, '0')
|
||||
val cleanedFiles = repo.files.map { it.copy(staged = false) }
|
||||
return repo.copy(
|
||||
files = cleanedFiles,
|
||||
commits = repo.commits + CommitNode(nextId, message),
|
||||
branches = repo.branches + (repo.headBranch to (repo.commits.size + 1)),
|
||||
) to listOf("[$nextId] $message")
|
||||
}
|
||||
|
||||
private fun statusLines(repo: RepoState): List<String> {
|
||||
val staged = repo.files.filter { it.staged }.map { "new file: ${it.name}" }
|
||||
val unstaged = repo.files.filterNot { it.staged }.map { "untracked: ${it.name}" }
|
||||
return buildList {
|
||||
add("On branch ${repo.headBranch}")
|
||||
if (staged.isEmpty() && unstaged.isEmpty()) {
|
||||
add("nothing to commit, working tree clean")
|
||||
} else {
|
||||
if (staged.isNotEmpty()) {
|
||||
add("Changes to be committed:")
|
||||
addAll(staged)
|
||||
}
|
||||
if (unstaged.isNotEmpty()) {
|
||||
add("Untracked files:")
|
||||
addAll(unstaged)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
325
app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt
Normal file
325
app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt
Normal file
@@ -0,0 +1,325 @@
|
||||
package com.kawomi.githugandroid
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FilterChipDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
private val AppBackground = Color(0xFF000000)
|
||||
private val PanelPrimary = Color(0xFF121212)
|
||||
private val PanelSecondary = Color(0xFF1E1E1E)
|
||||
private val PanelTertiary = Color(0xFF262626)
|
||||
private val TerminalBackground = Color(0xFF050505)
|
||||
private val TextPrimary = Color(0xFFFFFFFF)
|
||||
private val TextSecondary = Color(0xFFE6E6E6)
|
||||
private val TextMuted = Color(0xFFBDBDBD)
|
||||
private val Accent = Color(0xFF00E5FF)
|
||||
private val Success = Color(0xFF00FF95)
|
||||
|
||||
private val GitHugColorScheme = darkColorScheme(
|
||||
primary = Accent,
|
||||
onPrimary = Color.Black,
|
||||
secondary = TextPrimary,
|
||||
onSecondary = Color.Black,
|
||||
background = AppBackground,
|
||||
onBackground = TextPrimary,
|
||||
surface = PanelPrimary,
|
||||
onSurface = TextPrimary,
|
||||
surfaceVariant = PanelSecondary,
|
||||
onSurfaceVariant = TextSecondary,
|
||||
outline = TextMuted,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun GitHugApp() {
|
||||
MaterialTheme(colorScheme = GitHugColorScheme) {
|
||||
val levels = remember { sampleLevels() }
|
||||
val screenScrollState = rememberScrollState()
|
||||
var currentLevelIndex by remember { mutableStateOf(0) }
|
||||
var mode by remember { mutableStateOf(PlayMode.CLI_ONLY) }
|
||||
var repo by remember { mutableStateOf(levels.first().setup()) }
|
||||
var commandInput by remember { mutableStateOf("") }
|
||||
var output by remember { mutableStateOf(listOf("Welcome to GitHug Android.")) }
|
||||
var hintIndex by remember { mutableStateOf(0) }
|
||||
var completedLevels by remember { mutableStateOf(setOf<String>()) }
|
||||
val currentLevel = levels[currentLevelIndex]
|
||||
|
||||
val solved = currentLevel.validator(repo)
|
||||
|
||||
LaunchedEffect(currentLevelIndex) {
|
||||
screenScrollState.scrollTo(0)
|
||||
}
|
||||
|
||||
fun resetCurrentLevel(message: String = "Level reset.") {
|
||||
repo = currentLevel.setup()
|
||||
commandInput = ""
|
||||
output = listOf(message)
|
||||
hintIndex = 0
|
||||
}
|
||||
|
||||
fun runCommand() {
|
||||
val raw = commandInput.trim()
|
||||
if (raw.isBlank()) return
|
||||
val (newRepo, lines) = GitSandboxEngine.execute(repo, raw)
|
||||
val solvedAfterCommand = currentLevel.validator(newRepo)
|
||||
val wasAlreadyCompleted = currentLevel.id in completedLevels
|
||||
val newOutput = buildList {
|
||||
addAll(output)
|
||||
add("$ $raw")
|
||||
addAll(lines)
|
||||
if (solvedAfterCommand && !wasAlreadyCompleted) {
|
||||
add("✔ Level solved: ${currentLevel.title}")
|
||||
}
|
||||
}
|
||||
if (solvedAfterCommand && !wasAlreadyCompleted) {
|
||||
completedLevels = completedLevels + currentLevel.id
|
||||
val hasNextLevel = currentLevelIndex < levels.lastIndex
|
||||
if (hasNextLevel) {
|
||||
val nextLevelIndex = currentLevelIndex + 1
|
||||
val nextLevel = levels[nextLevelIndex]
|
||||
currentLevelIndex = nextLevelIndex
|
||||
repo = nextLevel.setup()
|
||||
output = listOf("Loaded level: ${nextLevel.title}")
|
||||
commandInput = ""
|
||||
hintIndex = 0
|
||||
} else {
|
||||
repo = newRepo
|
||||
output = listOf("🏁 All available MVP levels completed.")
|
||||
commandInput = ""
|
||||
}
|
||||
} else {
|
||||
repo = newRepo
|
||||
output = newOutput
|
||||
commandInput = ""
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold { padding ->
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(AppBackground)
|
||||
.verticalScroll(screenScrollState)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Header(levels, currentLevelIndex, completedLevels) { index ->
|
||||
currentLevelIndex = index
|
||||
repo = levels[index].setup()
|
||||
output = listOf("Loaded level: ${levels[index].title}")
|
||||
commandInput = ""
|
||||
hintIndex = 0
|
||||
}
|
||||
ModeBar(mode = mode, onModeSelected = { mode = it })
|
||||
LevelCard(
|
||||
level = currentLevel,
|
||||
onHint = {
|
||||
val hint = currentLevel.hints.getOrNull(hintIndex) ?: "No more hints for this level."
|
||||
output = output + "hint> $hint"
|
||||
hintIndex = (hintIndex + 1).coerceAtMost(currentLevel.hints.size)
|
||||
},
|
||||
onReset = { resetCurrentLevel() },
|
||||
)
|
||||
if (mode == PlayMode.VISUAL) {
|
||||
VisualizationPanel(repo)
|
||||
}
|
||||
TerminalPanel(
|
||||
output = output,
|
||||
commandInput = commandInput,
|
||||
onValueChange = { commandInput = it },
|
||||
onRun = { runCommand() },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 260.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Header(levels: List<Level>, currentLevelIndex: Int, completedLevels: Set<String>, onSelect: (Int) -> Unit) {
|
||||
Card(colors = CardDefaults.cardColors(containerColor = PanelPrimary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("GitHug Android", style = MaterialTheme.typography.headlineSmall, color = TextPrimary)
|
||||
Text("CLI-first Git learning on Android, with optional repository visualization.", color = TextSecondary)
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
levels.forEachIndexed { index, level ->
|
||||
TextButton(
|
||||
onClick = { onSelect(index) },
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = if (index == currentLevelIndex) Accent else TextSecondary
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = buildString {
|
||||
append(if (level.id in completedLevels) "✓ " else "• ")
|
||||
append(level.title)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModeBar(
|
||||
mode: PlayMode,
|
||||
onModeSelected: (PlayMode) -> Unit,
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
PlayMode.entries.forEach {
|
||||
FilterChip(
|
||||
selected = mode == it,
|
||||
onClick = { onModeSelected(it) },
|
||||
label = { Text(it.label) },
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
selectedContainerColor = Accent,
|
||||
selectedLabelColor = Color.Black,
|
||||
containerColor = PanelSecondary,
|
||||
labelColor = TextPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LevelCard(level: Level, onHint: () -> Unit, onReset: () -> Unit) {
|
||||
Card(colors = CardDefaults.cardColors(containerColor = PanelSecondary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Text(level.title, style = MaterialTheme.typography.titleLarge, color = TextPrimary)
|
||||
Text(level.description, color = TextSecondary)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = onHint,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Accent,
|
||||
contentColor = Color.Black
|
||||
)
|
||||
) { Text("Hint") }
|
||||
TextButton(
|
||||
onClick = onReset,
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = Accent)
|
||||
) { Text("Reset level") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VisualizationPanel(repo: RepoState) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
InfoCard("Workspace", repo.files.joinToString("\n") {
|
||||
"${if (it.staged) "[staged]" else "[file] "} ${it.name}"
|
||||
}.ifBlank { "(empty)" }, Modifier.weight(1f))
|
||||
InfoCard("Branches", buildString {
|
||||
appendLine("HEAD -> ${repo.headBranch}")
|
||||
repo.branches.forEach { (name, _) -> appendLine(name) }
|
||||
}.trim(), Modifier.weight(1f))
|
||||
InfoCard("Commits", repo.commits.reversed().joinToString("\n") { "${it.id} ${it.message}" }.ifBlank { "No commits yet" }, Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoCard(title: String, content: String, modifier: Modifier = Modifier) {
|
||||
Card(modifier = modifier, colors = CardDefaults.cardColors(containerColor = PanelSecondary), shape = RoundedCornerShape(16.dp)) {
|
||||
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(title, color = TextPrimary, fontWeight = FontWeight.Bold)
|
||||
Text(content, color = TextSecondary, fontFamily = FontFamily.Monospace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TerminalPanel(
|
||||
output: List<String>,
|
||||
commandInput: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
onRun: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Card(modifier = modifier, colors = CardDefaults.cardColors(containerColor = TerminalBackground)) {
|
||||
Box(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text("Terminal", color = TextPrimary, fontWeight = FontWeight.Bold)
|
||||
output.forEach { line ->
|
||||
Text(
|
||||
text = line,
|
||||
color = if (line.startsWith("✔") || line.startsWith("🏁")) Success else TextSecondary,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(PanelPrimary, RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "$",
|
||||
color = Accent,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
BasicTextField(
|
||||
value = commandInput,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
textStyle = TextStyle(color = TextPrimary, fontFamily = FontFamily.Monospace),
|
||||
cursorBrush = SolidColor(Accent),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
keyboardType = KeyboardType.Ascii,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { onRun() })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
app/src/main/java/com/kawomi/githugandroid/MainActivity.kt
Normal file
16
app/src/main/java/com/kawomi/githugandroid/MainActivity.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.kawomi.githugandroid
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
GitHugApp()
|
||||
}
|
||||
}
|
||||
}
|
||||
3
app/src/main/res/values/strings.xml
Normal file
3
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">GitHug Android</string>
|
||||
</resources>
|
||||
7
app/src/main/res/values/themes.xml
Normal file
7
app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="Theme.GithugAndroid" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/black</item>
|
||||
<item name="android:windowLightStatusBar" tools:targetApi="m">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
4
app/src/main/res/xml/backup_rules.xml
Normal file
4
app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<include domain="sharedpref" path="." />
|
||||
</full-backup-content>
|
||||
4
build.gradle.kts
Normal file
4
build.gradle.kts
Normal file
@@ -0,0 +1,4 @@
|
||||
plugins {
|
||||
id("com.android.application") version "8.5.2" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.9.24" apply false
|
||||
}
|
||||
5
gradle.properties
Normal file
5
gradle.properties
Normal file
@@ -0,0 +1,5 @@
|
||||
org.gradle.jvmargs=-Xmx1536m -Dfile.encoding=UTF-8
|
||||
org.gradle.daemon=false
|
||||
android.useAndroidX=true
|
||||
kotlin.code.style=official
|
||||
android.nonTransitiveRClass=true
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
12
gradlew
vendored
Executable file
12
gradlew
vendored
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
APP_HOME=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
else
|
||||
JAVACMD=java
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
||||
23
gradlew.bat
vendored
Normal file
23
gradlew.bat
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
@ECHO OFF
|
||||
SET DIRNAME=%~dp0
|
||||
IF "%DIRNAME%"=="" SET DIRNAME=.
|
||||
SET APP_HOME=%DIRNAME%
|
||||
SET CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
IF DEFINED JAVA_HOME GOTO findJavaFromJavaHome
|
||||
SET JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
IF %ERRORLEVEL% EQU 0 GOTO execute
|
||||
|
||||
ECHO ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
GOTO fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
SET JAVA_HOME=%JAVA_HOME:"=%
|
||||
SET JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
:execute
|
||||
"%JAVA_EXE%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:fail
|
||||
EXIT /B 1
|
||||
18
settings.gradle.kts
Normal file
18
settings.gradle.kts
Normal file
@@ -0,0 +1,18 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "Githug-Android"
|
||||
include(":app")
|
||||
Reference in New Issue
Block a user