From 82e38de3bb86770c6ceca29c88eaf98503b9ba9d Mon Sep 17 00:00:00 2001 From: Joe Tretter Date: Wed, 24 Jun 2026 20:32:23 -0500 Subject: [PATCH] Fix config level completion on device --- app/build.gradle.kts | 4 +- .../GitRepositoryRuntimeInstrumentedTest.kt | 20 +++++++- app/src/main/cpp/githug_runtime.c | 48 +++++++++++++++---- .../githugandroid/GitRepositoryInspector.kt | 24 ++++++++++ .../githugandroid/GitSandboxEngineTest.kt | 10 ++-- 5 files changed, 89 insertions(+), 17 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index af39d0c..4b57441 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,8 +20,8 @@ android { applicationId = "solutions.tretter.githugandroid" minSdk = 26 targetSdk = 35 - versionCode = 174 - versionName = "0.1.173" + versionCode = 175 + versionName = "0.1.174" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true diff --git a/app/src/androidTest/java/solutions/tretter/githugandroid/GitRepositoryRuntimeInstrumentedTest.kt b/app/src/androidTest/java/solutions/tretter/githugandroid/GitRepositoryRuntimeInstrumentedTest.kt index 4eabcce..9e14481 100644 --- a/app/src/androidTest/java/solutions/tretter/githugandroid/GitRepositoryRuntimeInstrumentedTest.kt +++ b/app/src/androidTest/java/solutions/tretter/githugandroid/GitRepositoryRuntimeInstrumentedTest.kt @@ -12,6 +12,7 @@ import androidx.test.core.app.ActivityScenario import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import java.io.File +import kotlinx.coroutines.runBlocking import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test @@ -48,10 +49,27 @@ class GitRepositoryRuntimeInstrumentedTest { } } + @Test + fun configLevelCompletesThroughUiWithArbitraryValues() { + launchFreshApp().use { + submitTerminalCommand("git init") + waitForExercise(configLevel()) + + submitTerminalCommand("git config user.name githug") + submitTerminalCommand("git config user.email xxx@yy.com") + + waitForLevelCompletion(nextLevel = addLevel()) + } + } + private fun launchFreshApp(): ActivityScenario { val context = InstrumentationRegistry.getInstrumentation().targetContext - File(context.applicationInfo.dataDir, "files").deleteRecursively() + File(context.filesDir, "githug-sandboxes").deleteRecursively() File(context.applicationInfo.dataDir, "cache").deleteRecursively() + runBlocking { + GameProgressStore(context.applicationContext).saveProgress(emptySet(), activeLevelId = null) + GameProgressStore(context.applicationContext).saveShowHelpOnStart(showHelpOnStart = true) + } val scenario = ActivityScenario.launch(MainActivity::class.java) waitForExercise(initLevel()) return scenario diff --git a/app/src/main/cpp/githug_runtime.c b/app/src/main/cpp/githug_runtime.c index 6149ec9..f6e656b 100644 --- a/app/src/main/cpp/githug_runtime.c +++ b/app/src/main/cpp/githug_runtime.c @@ -4,13 +4,16 @@ #include #include #include +#include #include #include #include #include +#include #include #define LOG_TAG "GitHugNativeGit" +#define GIT_COMMAND_TIMEOUT_MS 30000 typedef int (*git_main_fn)(int argc, const char **argv); typedef void (*git_init_fn)(const char **argv); @@ -73,6 +76,14 @@ static int drain_available_output(int fd, struct output_buffer *output, int *saw } } +static long long monotonic_millis(void) { + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) { + return 0; + } + return ((long long)now.tv_sec * 1000LL) + ((long long)now.tv_nsec / 1000000LL); +} + static jobjectArray make_result(JNIEnv *env, int exit_code, const char *output) { jclass string_class = (*env)->FindClass(env, "java/lang/String"); jobjectArray result = (*env)->NewObjectArray(env, 2, string_class, NULL); @@ -227,6 +238,17 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative( } pthread_mutex_lock(&git_mutex); + if (load_git(library_path_chars) != 0) { + const char *error = dlerror(); + close(pipe_fds[0]); + close(pipe_fds[1]); + pthread_mutex_unlock(&git_mutex); + free_string_array(argv, argc); + free_string_array(env_entries, envc); + (*env)->ReleaseStringUTFChars(env, library_path, library_path_chars); + (*env)->ReleaseStringUTFChars(env, working_directory, working_directory_chars); + return make_error(env, error != NULL ? error : "Native Git execution failed: init_git/cmd_main not found"); + } pid_t child = fork(); if (child < 0) { @@ -241,6 +263,7 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative( } if (child == 0) { + setpgid(0, 0); close(pipe_fds[0]); dup2(pipe_fds[1], STDOUT_FILENO); dup2(pipe_fds[1], STDERR_FILENO); @@ -251,20 +274,13 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative( (void)saved_env; chdir(working_directory_chars); - if (load_git(library_path_chars) != 0) { - const char *error = dlerror(); - fprintf(stderr, "%s\n", error != NULL ? error : "Native Git execution failed: init_git/cmd_main not found"); - fflush(stdout); - fflush(stderr); - _exit(127); - } - git_init((const char **)argv); int child_exit_code = git_main(argc, (const char **)argv); fflush(stdout); fflush(stderr); _exit(child_exit_code); } + setpgid(child, child); close(pipe_fds[1]); struct output_buffer output = {0}; @@ -276,6 +292,8 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative( int child_status = 0; int child_done = 0; int saw_eof = 0; + int timed_out = 0; + long long started_at = monotonic_millis(); while (!child_done || !saw_eof) { if (drain_available_output(pipe_fds[0], &output, &saw_eof) != 0) { break; @@ -288,6 +306,16 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative( child_done = 1; } } + if (!child_done && started_at > 0 && monotonic_millis() - started_at > GIT_COMMAND_TIMEOUT_MS) { + timed_out = 1; + kill(-child, SIGKILL); + kill(child, SIGKILL); + while (waitpid(child, &child_status, 0) < 0 && errno == EINTR) { + } + child_done = 1; + const char *timeout_message = "\nNative Git execution timed out.\n"; + append_output(&output, timeout_message, strlen(timeout_message)); + } if (child_done) { if (!saw_eof) { drain_available_output(pipe_fds[0], &output, &saw_eof); @@ -300,7 +328,9 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative( pthread_mutex_unlock(&git_mutex); int exit_code = -1; - if (WIFEXITED(child_status)) { + if (timed_out) { + exit_code = 124; + } else if (WIFEXITED(child_status)) { exit_code = WEXITSTATUS(child_status); } else if (WIFSIGNALED(child_status)) { exit_code = 128 + WTERMSIG(child_status); diff --git a/app/src/main/java/solutions/tretter/githugandroid/GitRepositoryInspector.kt b/app/src/main/java/solutions/tretter/githugandroid/GitRepositoryInspector.kt index fb3c208..e8d3372 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/GitRepositoryInspector.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/GitRepositoryInspector.kt @@ -200,9 +200,33 @@ internal class GitRepositoryInspector( userEmailResult.outputLines.firstOrNull() ?.takeIf { userEmailResult.exitCode == 0 && it.isNotBlank() } ?.let { put("user.email", it) } + putAll(readGitConfigFile(inspectionRoot).filterKeys { it !in keys }) } } + private fun readGitConfigFile(inspectionRoot: File): Map { + val configFile = File(inspectionRoot, ".git/config") + if (!configFile.isFile) return emptyMap() + + val entries = linkedMapOf() + var section = "" + configFile.forEachLine { rawLine -> + val line = rawLine.trim() + when { + line.isBlank() || line.startsWith("#") || line.startsWith(";") -> return@forEachLine + line.startsWith("[") && line.endsWith("]") -> { + section = line.removePrefix("[").removeSuffix("]").trim().substringBefore(' ') + } + "=" in line && section.isNotBlank() -> { + val key = line.substringBefore('=').trim() + val value = line.substringAfter('=').trim() + entries["$section.$key"] = value + } + } + } + return entries + } + private fun inspectRemoteRefs(git: File, inspectionRoot: File, remoteLines: List): RemoteRefs { val remotes = remoteLines.mapNotNull { line -> val parts = line.trim().split(Regex("\\s+")) diff --git a/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt b/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt index 6c6108e..6720b2c 100644 --- a/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt +++ b/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt @@ -268,12 +268,12 @@ class GitSandboxEngineTest { val level = configLevel() val repo = runtime.prepareLevel(level) - val (namedRepo, _) = runtime.execute(level, repo, "git config user.name GitHug") - val (configuredRepo, _) = runtime.execute(level, namedRepo, "git config user.email githug@example.com") + val (namedRepo, _) = runtime.execute(level, repo, "git config user.name githug") + val (configuredRepo, _) = runtime.execute(level, namedRepo, "git config user.email xxx@yy.com") - assertEquals("GitHug", configuredRepo.config["user.name"]) - assertEquals("githug@example.com", configuredRepo.config["user.email"]) - assertTrue(level.validator(configuredRepo, "git config user.email githug@example.com")) + assertEquals("githug", configuredRepo.config["user.name"]) + assertEquals("xxx@yy.com", configuredRepo.config["user.email"]) + assertTrue(level.validator(configuredRepo, "git config user.email xxx@yy.com")) } finally { root.deleteRecursively() }