Fix config level completion on device

This commit is contained in:
Joe Tretter
2026-06-24 20:32:23 -05:00
parent 1e87bd9aa7
commit 82e38de3bb
5 changed files with 89 additions and 17 deletions

View File

@@ -20,8 +20,8 @@ android {
applicationId = "solutions.tretter.githugandroid" applicationId = "solutions.tretter.githugandroid"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 174 versionCode = 175
versionName = "0.1.173" versionName = "0.1.174"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true

View File

@@ -12,6 +12,7 @@ import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import java.io.File import java.io.File
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Rule import org.junit.Rule
import org.junit.Test 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<MainActivity> { private fun launchFreshApp(): ActivityScenario<MainActivity> {
val context = InstrumentationRegistry.getInstrumentation().targetContext val context = InstrumentationRegistry.getInstrumentation().targetContext
File(context.applicationInfo.dataDir, "files").deleteRecursively() File(context.filesDir, "githug-sandboxes").deleteRecursively()
File(context.applicationInfo.dataDir, "cache").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) val scenario = ActivityScenario.launch(MainActivity::class.java)
waitForExercise(initLevel()) waitForExercise(initLevel())
return scenario return scenario

View File

@@ -4,13 +4,16 @@
#include <errno.h> #include <errno.h>
#include <fcntl.h> #include <fcntl.h>
#include <pthread.h> #include <pthread.h>
#include <signal.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <time.h>
#include <unistd.h> #include <unistd.h>
#define LOG_TAG "GitHugNativeGit" #define LOG_TAG "GitHugNativeGit"
#define GIT_COMMAND_TIMEOUT_MS 30000
typedef int (*git_main_fn)(int argc, const char **argv); typedef int (*git_main_fn)(int argc, const char **argv);
typedef void (*git_init_fn)(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) { static jobjectArray make_result(JNIEnv *env, int exit_code, const char *output) {
jclass string_class = (*env)->FindClass(env, "java/lang/String"); jclass string_class = (*env)->FindClass(env, "java/lang/String");
jobjectArray result = (*env)->NewObjectArray(env, 2, string_class, NULL); jobjectArray result = (*env)->NewObjectArray(env, 2, string_class, NULL);
@@ -227,6 +238,17 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative(
} }
pthread_mutex_lock(&git_mutex); 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(); pid_t child = fork();
if (child < 0) { if (child < 0) {
@@ -241,6 +263,7 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative(
} }
if (child == 0) { if (child == 0) {
setpgid(0, 0);
close(pipe_fds[0]); close(pipe_fds[0]);
dup2(pipe_fds[1], STDOUT_FILENO); dup2(pipe_fds[1], STDOUT_FILENO);
dup2(pipe_fds[1], STDERR_FILENO); dup2(pipe_fds[1], STDERR_FILENO);
@@ -251,20 +274,13 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative(
(void)saved_env; (void)saved_env;
chdir(working_directory_chars); 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); git_init((const char **)argv);
int child_exit_code = git_main(argc, (const char **)argv); int child_exit_code = git_main(argc, (const char **)argv);
fflush(stdout); fflush(stdout);
fflush(stderr); fflush(stderr);
_exit(child_exit_code); _exit(child_exit_code);
} }
setpgid(child, child);
close(pipe_fds[1]); close(pipe_fds[1]);
struct output_buffer output = {0}; struct output_buffer output = {0};
@@ -276,6 +292,8 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative(
int child_status = 0; int child_status = 0;
int child_done = 0; int child_done = 0;
int saw_eof = 0; int saw_eof = 0;
int timed_out = 0;
long long started_at = monotonic_millis();
while (!child_done || !saw_eof) { while (!child_done || !saw_eof) {
if (drain_available_output(pipe_fds[0], &output, &saw_eof) != 0) { if (drain_available_output(pipe_fds[0], &output, &saw_eof) != 0) {
break; break;
@@ -288,6 +306,16 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative(
child_done = 1; 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 (child_done) {
if (!saw_eof) { if (!saw_eof) {
drain_available_output(pipe_fds[0], &output, &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); pthread_mutex_unlock(&git_mutex);
int exit_code = -1; int exit_code = -1;
if (WIFEXITED(child_status)) { if (timed_out) {
exit_code = 124;
} else if (WIFEXITED(child_status)) {
exit_code = WEXITSTATUS(child_status); exit_code = WEXITSTATUS(child_status);
} else if (WIFSIGNALED(child_status)) { } else if (WIFSIGNALED(child_status)) {
exit_code = 128 + WTERMSIG(child_status); exit_code = 128 + WTERMSIG(child_status);

View File

@@ -200,9 +200,33 @@ internal class GitRepositoryInspector(
userEmailResult.outputLines.firstOrNull() userEmailResult.outputLines.firstOrNull()
?.takeIf { userEmailResult.exitCode == 0 && it.isNotBlank() } ?.takeIf { userEmailResult.exitCode == 0 && it.isNotBlank() }
?.let { put("user.email", it) } ?.let { put("user.email", it) }
putAll(readGitConfigFile(inspectionRoot).filterKeys { it !in keys })
} }
} }
private fun readGitConfigFile(inspectionRoot: File): Map<String, String> {
val configFile = File(inspectionRoot, ".git/config")
if (!configFile.isFile) return emptyMap()
val entries = linkedMapOf<String, String>()
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<String>): RemoteRefs { private fun inspectRemoteRefs(git: File, inspectionRoot: File, remoteLines: List<String>): RemoteRefs {
val remotes = remoteLines.mapNotNull { line -> val remotes = remoteLines.mapNotNull { line ->
val parts = line.trim().split(Regex("\\s+")) val parts = line.trim().split(Regex("\\s+"))

View File

@@ -268,12 +268,12 @@ class GitSandboxEngineTest {
val level = configLevel() val level = configLevel()
val repo = runtime.prepareLevel(level) val repo = runtime.prepareLevel(level)
val (namedRepo, _) = runtime.execute(level, repo, "git config user.name GitHug") 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 (configuredRepo, _) = runtime.execute(level, namedRepo, "git config user.email xxx@yy.com")
assertEquals("GitHug", configuredRepo.config["user.name"]) assertEquals("githug", configuredRepo.config["user.name"])
assertEquals("githug@example.com", configuredRepo.config["user.email"]) assertEquals("xxx@yy.com", configuredRepo.config["user.email"])
assertTrue(level.validator(configuredRepo, "git config user.email githug@example.com")) assertTrue(level.validator(configuredRepo, "git config user.email xxx@yy.com"))
} finally { } finally {
root.deleteRecursively() root.deleteRecursively()
} }