Route interactive Git through native terminal sessions

Remove Kotlin interactive-add emulation and run patch add via the native Git runtime with PTY-backed session IO.
This commit is contained in:
Joe Tretter
2026-06-27 13:07:35 -05:00
parent d703f539d3
commit 943bf03ca0
19 changed files with 684 additions and 549 deletions

View File

@@ -8,12 +8,14 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#define LOG_TAG "GitHugNativeGit"
#define GIT_COMMAND_TIMEOUT_MS 30000
#define GIT_SESSION_MAX_READ_WAIT_MS 1000
typedef int (*git_main_fn)(int argc, const char **argv);
typedef int (*githug_git_main_fn)(int argc, const char **argv);
@@ -31,6 +33,16 @@ static git_main_fn git_main = NULL;
static githug_git_main_fn githug_git_main = NULL;
static git_init_fn git_init = NULL;
struct git_session {
int id;
pid_t child;
int pty_fd;
struct git_session *next;
};
static struct git_session *git_sessions = NULL;
static int next_git_session_id = 1;
static int append_output(struct output_buffer *buffer, const char *data, size_t length) {
if (length == 0) {
return 0;
@@ -100,6 +112,34 @@ static jobjectArray make_result(JNIEnv *env, int exit_code, const char *output)
return result;
}
static jobjectArray make_session_result(JNIEnv *env, int session_id, int running, int exit_code, const char *output) {
jclass string_class = (*env)->FindClass(env, "java/lang/String");
jobjectArray result = (*env)->NewObjectArray(env, 4, string_class, NULL);
char session_text[32];
char running_text[8];
char exit_text[32];
snprintf(session_text, sizeof(session_text), "%d", session_id);
snprintf(running_text, sizeof(running_text), "%d", running ? 1 : 0);
if (exit_code >= 0) {
snprintf(exit_text, sizeof(exit_text), "%d", exit_code);
} else {
exit_text[0] = '\0';
}
jstring session_string = (*env)->NewStringUTF(env, session_text);
jstring running_string = (*env)->NewStringUTF(env, running_text);
jstring exit_string = (*env)->NewStringUTF(env, exit_text);
jstring output_string = (*env)->NewStringUTF(env, output != NULL ? output : "");
(*env)->SetObjectArrayElement(env, result, 0, session_string);
(*env)->SetObjectArrayElement(env, result, 1, running_string);
(*env)->SetObjectArrayElement(env, result, 2, exit_string);
(*env)->SetObjectArrayElement(env, result, 3, output_string);
(*env)->DeleteLocalRef(env, session_string);
(*env)->DeleteLocalRef(env, running_string);
(*env)->DeleteLocalRef(env, exit_string);
(*env)->DeleteLocalRef(env, output_string);
return result;
}
static jobjectArray make_error(JNIEnv *env, const char *message) {
return make_result(env, -1, message);
}
@@ -161,6 +201,45 @@ static void free_string_array(char **values, int count) {
free(values);
}
static struct git_session *find_session(int session_id, struct git_session ***link_out) {
struct git_session **link = &git_sessions;
while (*link != NULL) {
if ((*link)->id == session_id) {
if (link_out != NULL) {
*link_out = link;
}
return *link;
}
link = &((*link)->next);
}
return NULL;
}
static void remove_session(struct git_session **link) {
if (link == NULL) {
return;
}
struct git_session *session = *link;
if (session == NULL) {
return;
}
*link = session->next;
if (session->pty_fd >= 0) {
close(session->pty_fd);
}
free(session);
}
static int child_exit_code_from_status(int status) {
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
}
if (WIFSIGNALED(status)) {
return 128 + WTERMSIG(status);
}
return -1;
}
struct saved_env {
char *name;
char *previous;
@@ -353,3 +432,220 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative(
(*env)->ReleaseStringUTFChars(env, working_directory, working_directory_chars);
return result;
}
JNIEXPORT jobjectArray JNICALL
Java_solutions_tretter_githugandroid_NativeGitBridge_startGitSessionNative(
JNIEnv *env,
jclass clazz,
jstring library_path,
jstring working_directory,
jobjectArray argv_array,
jobjectArray environment_array
) {
(void)clazz;
const char *library_path_chars = (*env)->GetStringUTFChars(env, library_path, NULL);
const char *working_directory_chars = (*env)->GetStringUTFChars(env, working_directory, NULL);
int argc = 0;
int envc = 0;
char **argv = copy_string_array(env, argv_array, &argc);
char **env_entries = copy_string_array(env, environment_array, &envc);
if (library_path_chars == NULL || working_directory_chars == NULL || argv == NULL || env_entries == NULL) {
return make_session_result(env, 0, 0, -1, "Native Git session failed: out of memory");
}
int master_fd = posix_openpt(O_RDWR | O_NOCTTY);
if (master_fd < 0 || grantpt(master_fd) != 0 || unlockpt(master_fd) != 0) {
if (master_fd >= 0) {
close(master_fd);
}
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_session_result(env, 0, 0, -1, "Native Git session failed: could not create PTY");
}
char *slave_name = ptsname(master_fd);
if (slave_name == NULL) {
close(master_fd);
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_session_result(env, 0, 0, -1, "Native Git session failed: could not resolve PTY slave");
}
pthread_mutex_lock(&git_mutex);
if (load_git(library_path_chars) != 0) {
close(master_fd);
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_session_result(env, 0, 0, -1, "Native Git session failed: init_git/cmd_main not found");
}
pid_t child = fork();
if (child < 0) {
close(master_fd);
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_session_result(env, 0, 0, -1, "Native Git session failed: could not fork");
}
if (child == 0) {
setsid();
int slave_fd = open(slave_name, O_RDWR);
if (slave_fd < 0) {
_exit(127);
}
ioctl(slave_fd, TIOCSCTTY, 0);
dup2(slave_fd, STDIN_FILENO);
dup2(slave_fd, STDOUT_FILENO);
dup2(slave_fd, STDERR_FILENO);
if (slave_fd > STDERR_FILENO) {
close(slave_fd);
}
close(master_fd);
int applied_env = 0;
struct saved_env *saved_env = apply_environment(env_entries, envc, &applied_env);
(void)saved_env;
chdir(working_directory_chars);
int child_exit_code;
if (githug_git_main != NULL) {
child_exit_code = githug_git_main(argc, (const char **)argv);
} else {
git_init((const char **)argv);
child_exit_code = git_main(argc, (const char **)argv);
}
fflush(stdout);
fflush(stderr);
_exit(child_exit_code);
}
setpgid(child, child);
int flags = fcntl(master_fd, F_GETFL, 0);
if (flags >= 0) {
fcntl(master_fd, F_SETFL, flags | O_NONBLOCK);
}
struct git_session *session = calloc(1, sizeof(struct git_session));
if (session == NULL) {
kill(-child, SIGKILL);
close(master_fd);
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_session_result(env, 0, 0, -1, "Native Git session failed: out of memory");
}
session->id = next_git_session_id++;
session->child = child;
session->pty_fd = master_fd;
session->next = git_sessions;
git_sessions = session;
struct output_buffer output = {0};
int saw_eof = 0;
long long started_at = monotonic_millis();
while (started_at > 0 && monotonic_millis() - started_at < GIT_SESSION_MAX_READ_WAIT_MS) {
drain_available_output(session->pty_fd, &output, &saw_eof);
if (output.length > 0) {
break;
}
usleep(10000);
}
int session_id = session->id;
int status = 0;
int running = 1;
int exit_code = -1;
pid_t wait_result = waitpid(child, &status, WNOHANG);
if (wait_result == child) {
running = 0;
exit_code = child_exit_code_from_status(status);
struct git_session **link = NULL;
find_session(session_id, &link);
remove_session(link);
}
jobjectArray result = make_session_result(env, session_id, running, exit_code, output.data);
free(output.data);
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 result;
}
JNIEXPORT jobjectArray JNICALL
Java_solutions_tretter_githugandroid_NativeGitBridge_writeGitSessionNative(
JNIEnv *env,
jclass clazz,
jint session_id,
jstring input
) {
(void)clazz;
const char *input_chars = (*env)->GetStringUTFChars(env, input, NULL);
pthread_mutex_lock(&git_mutex);
struct git_session **link = NULL;
struct git_session *session = find_session((int)session_id, &link);
if (session == NULL) {
pthread_mutex_unlock(&git_mutex);
if (input_chars != NULL) {
(*env)->ReleaseStringUTFChars(env, input, input_chars);
}
return make_session_result(env, (int)session_id, 0, -1, "Native Git session is not running.");
}
if (input_chars != NULL) {
size_t input_length = strlen(input_chars);
size_t written = 0;
while (written < input_length) {
ssize_t count = write(session->pty_fd, input_chars + written, input_length - written);
if (count > 0) {
written += (size_t)count;
} else if (errno != EINTR) {
break;
}
}
(*env)->ReleaseStringUTFChars(env, input, input_chars);
}
struct output_buffer output = {0};
int saw_eof = 0;
long long started_at = monotonic_millis();
while (started_at > 0 && monotonic_millis() - started_at < GIT_SESSION_MAX_READ_WAIT_MS) {
drain_available_output(session->pty_fd, &output, &saw_eof);
if (output.length > 0 || saw_eof) {
break;
}
usleep(10000);
}
int status = 0;
int running = 1;
int exit_code = -1;
pid_t wait_result = waitpid(session->child, &status, WNOHANG);
if (wait_result == session->child || saw_eof) {
if (wait_result != session->child) {
while (waitpid(session->child, &status, 0) < 0 && errno == EINTR) {
}
}
running = 0;
exit_code = child_exit_code_from_status(status);
remove_session(link);
}
jobjectArray result = make_session_result(env, (int)session_id, running, exit_code, output.data);
free(output.data);
pthread_mutex_unlock(&git_mutex);
return result;
}