mirror of
https://github.com/marcusDenslow/FERRUM.git
synced 2026-10-25 14:38:50 +01:00
had a problem where the commit info was cluttered together with only 1 new line between eachother making it hard to read, fixed this with some propper formatting and now things are actually readable
547 lines
12 KiB
C
547 lines
12 KiB
C
/**
|
|
* git_integration.c
|
|
* Implementation of Git repository detection and information
|
|
*/
|
|
|
|
#include "git_integration.h"
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
/**
|
|
* Initialize Git integration
|
|
*/
|
|
void init_git_integration(void) {
|
|
// No initialization needed for Linux
|
|
}
|
|
|
|
/**
|
|
* Check if the current directory is in a Git repository and get branch info
|
|
*/
|
|
int get_git_branch(char *branch_name, size_t buffer_size, int *is_dirty) {
|
|
char git_dir[PATH_MAX] = "";
|
|
char cmd[PATH_MAX] = "";
|
|
FILE *fp;
|
|
int status = 0;
|
|
|
|
// Initialize output parameters
|
|
if (branch_name && buffer_size > 0) {
|
|
branch_name[0] = '\0';
|
|
}
|
|
if (is_dirty) {
|
|
*is_dirty = 0;
|
|
}
|
|
|
|
// First check if .git directory exists (faster than running git command)
|
|
strcpy(git_dir, ".git");
|
|
struct stat st;
|
|
if (stat(git_dir, &st) != 0 || !S_ISDIR(st.st_mode)) {
|
|
// Try to find .git in parent directories
|
|
// (Note: this is more complex, simplified here)
|
|
return 0;
|
|
}
|
|
|
|
// Get current branch
|
|
if (branch_name && buffer_size > 0) {
|
|
strcpy(cmd, "git rev-parse --abbrev-ref HEAD 2>/dev/null");
|
|
fp = popen(cmd, "r");
|
|
if (fp) {
|
|
if (fgets(branch_name, buffer_size, fp) != NULL) {
|
|
// Remove trailing newline
|
|
char *newline = strchr(branch_name, '\n');
|
|
if (newline) {
|
|
*newline = '\0';
|
|
}
|
|
status = 1;
|
|
}
|
|
pclose(fp);
|
|
}
|
|
}
|
|
|
|
// Check if working directory is dirty
|
|
if (is_dirty && status) {
|
|
strcpy(cmd, "git status --porcelain 2>/dev/null");
|
|
fp = popen(cmd, "r");
|
|
if (fp) {
|
|
char ch;
|
|
if ((ch = fgetc(fp)) != EOF) {
|
|
*is_dirty = 1;
|
|
}
|
|
pclose(fp);
|
|
}
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
/**
|
|
* Get the name of the Git repository
|
|
*/
|
|
int get_git_repo_name(char *repo_name, size_t buffer_size) {
|
|
if (!repo_name || buffer_size == 0) {
|
|
return 0;
|
|
}
|
|
|
|
repo_name[0] = '\0';
|
|
|
|
char cmd[PATH_MAX] = "git rev-parse --show-toplevel 2>/dev/null";
|
|
FILE *fp = popen(cmd, "r");
|
|
if (!fp) {
|
|
return 0;
|
|
}
|
|
|
|
char path[PATH_MAX];
|
|
if (fgets(path, sizeof(path), fp) == NULL) {
|
|
pclose(fp);
|
|
return 0;
|
|
}
|
|
pclose(fp);
|
|
|
|
// Remove trailing newline
|
|
char *newline = strchr(path, '\n');
|
|
if (newline) {
|
|
*newline = '\0';
|
|
}
|
|
|
|
// Extract the directory name from the full path
|
|
char *last_slash = strrchr(path, '/');
|
|
if (last_slash) {
|
|
strncpy(repo_name, last_slash + 1, buffer_size - 1);
|
|
repo_name[buffer_size - 1] = '\0';
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Get Git status for the current repository
|
|
*/
|
|
char *get_git_status(void) {
|
|
char branch_name[100] = "";
|
|
int is_dirty = 0;
|
|
char repo_name[100] = "";
|
|
|
|
// Check if we're in a Git repo and get branch info
|
|
if (!get_git_branch(branch_name, sizeof(branch_name), &is_dirty)) {
|
|
return NULL; // Not in a Git repo
|
|
}
|
|
|
|
// Get repo name
|
|
get_git_repo_name(repo_name, sizeof(repo_name));
|
|
|
|
// Format status string
|
|
char *status = malloc(256);
|
|
if (!status) {
|
|
return NULL;
|
|
}
|
|
|
|
if (strlen(repo_name) > 0) {
|
|
sprintf(status, "%s%s (%s%s%s)", repo_name, branch_name[0] ? ":" : "",
|
|
branch_name, is_dirty ? " *" : "", "");
|
|
} else {
|
|
sprintf(status, "%s%s", branch_name, is_dirty ? " *" : "");
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
int get_last_commit(char *title, size_t title_size, char *hash,
|
|
size_t hash_size) {
|
|
if (!title || !hash || title_size == 0 || hash_size == 0) {
|
|
return 0;
|
|
}
|
|
title[0] = '\0';
|
|
hash[0] = '\0';
|
|
|
|
FILE *fp = popen("git rev-parse --short HEAD 2>/dev/null", "r");
|
|
if (fp) {
|
|
if (fgets(hash, hash_size, fp) != NULL) {
|
|
char *newline = strchr(hash, '\n');
|
|
if (newline)
|
|
*newline = '\0';
|
|
}
|
|
pclose(fp);
|
|
}
|
|
|
|
fp = popen("git log -1 --pretty=format:%s 2>/dev/null", "r");
|
|
if (fp) {
|
|
if (fgets(title, title_size, fp) != NULL) {
|
|
// No need to remove quotes since we're not using them in the format.
|
|
}
|
|
pclose(fp);
|
|
}
|
|
|
|
return (strlen(hash) > 0 && strlen(title) > 0) ? 1 : 0;
|
|
}
|
|
|
|
int get_recent_commit(char commits[][256], int count) {
|
|
if (!commits || count <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
char cmd[256];
|
|
snprintf(cmd, sizeof(cmd), "git log -%d --pretty=format:%%s 2>/dev/null",
|
|
count);
|
|
|
|
FILE *fp = popen(cmd, "r");
|
|
if (!fp) {
|
|
return 0;
|
|
}
|
|
|
|
int retrieved = 0;
|
|
while (retrieved < count && fgets(commits[retrieved], 256, fp) != NULL) {
|
|
char *newline = strchr(commits[retrieved], '\n');
|
|
if (newline)
|
|
*newline = '\0';
|
|
|
|
retrieved++;
|
|
}
|
|
|
|
pclose(fp);
|
|
return retrieved;
|
|
}
|
|
|
|
int get_repo_url(char *url, size_t url_size) {
|
|
if (!url || url_size == 0) {
|
|
return 0;
|
|
}
|
|
|
|
url[0] = '\0';
|
|
|
|
FILE *fp = popen("git config --get remote.origin.url 2>/dev/null", "r");
|
|
if (!fp) {
|
|
return 0;
|
|
}
|
|
|
|
char remote_url[512];
|
|
if (fgets(remote_url, sizeof(remote_url), fp) == NULL) {
|
|
pclose(fp);
|
|
return 0;
|
|
}
|
|
pclose(fp);
|
|
|
|
char *newline = strchr(remote_url, '\n');
|
|
if (newline)
|
|
*newline = '\0';
|
|
|
|
if (strstr(remote_url, "git@github.com:")) {
|
|
char *repo_path = strchr(remote_url, ':');
|
|
if (repo_path) {
|
|
repo_path++;
|
|
char *git_suffix = strstr(repo_path, ".git");
|
|
if (git_suffix)
|
|
*git_suffix = '\0';
|
|
|
|
snprintf(url, url_size, "https://github.com/%s", repo_path);
|
|
return 1;
|
|
}
|
|
} else if (strstr(remote_url, "https://github.com/")) {
|
|
strncpy(url, remote_url, url_size - 1);
|
|
url[url_size - 1] = '\0';
|
|
char *git_suffix = strstr(url, ".git");
|
|
if (git_suffix)
|
|
*git_suffix = '\0';
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Check if the current branch has diverged from its remote tracking branch
|
|
*/
|
|
int check_branch_divergence(int *commits_ahead, int *commits_behind) {
|
|
if (!commits_ahead || !commits_behind) {
|
|
return 0;
|
|
}
|
|
|
|
*commits_ahead = 0;
|
|
*commits_behind = 0;
|
|
|
|
// Check if we have a remote tracking branch
|
|
FILE *fp = popen("git rev-parse --abbrev-ref @{u} 2>/dev/null", "r");
|
|
if (!fp) {
|
|
return 0;
|
|
}
|
|
|
|
char remote_branch[256];
|
|
if (fgets(remote_branch, sizeof(remote_branch), fp) == NULL) {
|
|
pclose(fp);
|
|
return 0; // No remote tracking branch
|
|
}
|
|
pclose(fp);
|
|
|
|
// Remove newline
|
|
char *newline = strchr(remote_branch, '\n');
|
|
if (newline) {
|
|
*newline = '\0';
|
|
}
|
|
|
|
// Get commits ahead (local commits not in remote)
|
|
fp = popen("git rev-list --count @{u}..HEAD 2>/dev/null", "r");
|
|
if (fp) {
|
|
char count_str[32];
|
|
if (fgets(count_str, sizeof(count_str), fp) != NULL) {
|
|
*commits_ahead = atoi(count_str);
|
|
}
|
|
pclose(fp);
|
|
}
|
|
|
|
// Get commits behind (remote commits not in local)
|
|
fp = popen("git rev-list --count HEAD..@{u} 2>/dev/null", "r");
|
|
if (fp) {
|
|
char count_str[32];
|
|
if (fgets(count_str, sizeof(count_str), fp) != NULL) {
|
|
*commits_behind = atoi(count_str);
|
|
}
|
|
pclose(fp);
|
|
}
|
|
|
|
// Branch has diverged if we have commits both ahead and behind
|
|
return (*commits_ahead > 0 && *commits_behind > 0) ? 1 : 0;
|
|
}
|
|
|
|
int create_git_stash(void) {
|
|
FILE *fp = popen("git status --porcelain 2>/dev/null", "r");
|
|
if (!fp)
|
|
return 0;
|
|
|
|
char line[256];
|
|
int has_changes = 0;
|
|
if (fgets(line, sizeof(line), fp) != NULL) {
|
|
has_changes = 1;
|
|
}
|
|
pclose(fp);
|
|
|
|
if (!has_changes) {
|
|
return 0;
|
|
}
|
|
|
|
time_t now = time(NULL);
|
|
struct tm *timeinfo = localtime(&now);
|
|
|
|
char cmd[512];
|
|
snprintf(cmd, sizeof(cmd),
|
|
"git stash push -m \"WIP: stashed at %04d-%02d-%02d "
|
|
"%02d:%02d:%02d\" 2>/dev/null >/dev/null",
|
|
timeinfo->tm_year + 1900, timeinfo->tm_mon + 1, timeinfo->tm_mday,
|
|
timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);
|
|
|
|
int result = system(cmd);
|
|
return (result == 0) ? 1 : 0;
|
|
}
|
|
|
|
int create_git_stash_with_name(const char *stash_name) {
|
|
if (!stash_name || strlen(stash_name) == 0) {
|
|
return 0;
|
|
}
|
|
|
|
FILE *fp = popen("git status --porcelain 2>/dev/null", "r");
|
|
if (!fp)
|
|
return 0;
|
|
|
|
char line[256];
|
|
int has_changes = 0;
|
|
if (fgets(line, sizeof(line), fp) != NULL) {
|
|
has_changes = 1;
|
|
}
|
|
pclose(fp);
|
|
|
|
if (!has_changes) {
|
|
return 0;
|
|
}
|
|
|
|
char cmd[512];
|
|
snprintf(cmd, sizeof(cmd), "git stash push -m \"%s\" 2>/dev/null >/dev/null",
|
|
stash_name);
|
|
|
|
int result = system(cmd);
|
|
return (result == 0) ? 1 : 0;
|
|
}
|
|
|
|
int get_git_stashes(char stashes[][512], int max_stashes) {
|
|
if (!stashes || max_stashes <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
FILE *fp = popen("git stash list --format=\"%gd: %gs\" 2>/dev/null", "r");
|
|
if (!fp) {
|
|
return 0;
|
|
}
|
|
|
|
int count = 0;
|
|
char line[512];
|
|
|
|
while (fgets(line, sizeof(line), fp) != NULL && count < max_stashes) {
|
|
char *newline = strchr(line, '\n');
|
|
if (newline) {
|
|
*newline = '\0';
|
|
}
|
|
|
|
strncpy(stashes[count], line, 511);
|
|
stashes[count][511] = '\0';
|
|
count++;
|
|
}
|
|
|
|
pclose(fp);
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Apply a git stash by index (keeps stash in list)
|
|
*/
|
|
int apply_git_stash(int stash_index) {
|
|
if (stash_index < 0) {
|
|
return 0;
|
|
}
|
|
|
|
char cmd[128];
|
|
snprintf(cmd, sizeof(cmd),
|
|
"git stash apply stash@{%d} 2>/dev/null >/dev/null", stash_index);
|
|
|
|
int result = system(cmd);
|
|
return (result == 0) ? 1 : 0;
|
|
}
|
|
|
|
/**
|
|
* Pop a git stash by index (applies and removes stash from list)
|
|
*/
|
|
int pop_git_stash(int stash_index) {
|
|
if (stash_index < 0) {
|
|
return 0;
|
|
}
|
|
|
|
char cmd[128];
|
|
snprintf(cmd, sizeof(cmd), "git stash pop stash@{%d} 2>/dev/null >/dev/null",
|
|
stash_index);
|
|
|
|
int result = system(cmd);
|
|
return (result == 0) ? 1 : 0;
|
|
}
|
|
|
|
/**
|
|
* Drop a git stash by index (removes stash without applying)
|
|
*/
|
|
int drop_git_stash(int stash_index) {
|
|
if (stash_index < 0) {
|
|
return 0;
|
|
}
|
|
|
|
char cmd[128];
|
|
snprintf(cmd, sizeof(cmd), "git stash drop stash@{%d} 2>/dev/null >/dev/null",
|
|
stash_index);
|
|
|
|
int result = system(cmd);
|
|
return (result == 0) ? 1 : 0;
|
|
}
|
|
|
|
/**
|
|
* Get detailed commit information including diff
|
|
*/
|
|
int get_commit_details(const char *commit_hash, char *commit_info,
|
|
size_t info_size) {
|
|
if (!commit_hash || !commit_info || info_size == 0) {
|
|
return 0;
|
|
}
|
|
|
|
char cmd[512];
|
|
snprintf(
|
|
cmd, sizeof(cmd),
|
|
"git show --stat=120 --format=\"commit %%H %%d%%nAuthor: %%an "
|
|
"<%%ae>%%nDate: %%ad%%n%%n %%s%%n%%n %%b %%n --\" %s 2>/dev/null",
|
|
commit_hash);
|
|
|
|
FILE *fp = popen(cmd, "r");
|
|
if (!fp) {
|
|
return 0;
|
|
}
|
|
|
|
size_t total_read = 0;
|
|
char buffer[1024];
|
|
|
|
while (fgets(buffer, sizeof(buffer), fp) != NULL &&
|
|
total_read < info_size - 1) {
|
|
size_t line_len = strlen(buffer);
|
|
if (total_read + line_len < info_size - 1) {
|
|
strcpy(commit_info + total_read, buffer);
|
|
total_read += line_len;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
commit_info[total_read] = '\0';
|
|
pclose(fp);
|
|
|
|
// Now get the full diff content with all the @@ hunks and +/- lines
|
|
snprintf(cmd, sizeof(cmd), "git diff %s^..%s 2>/dev/null", commit_hash,
|
|
commit_hash);
|
|
fp = popen(cmd, "r");
|
|
if (!fp) {
|
|
return total_read > 0 ? 1 : 0;
|
|
}
|
|
|
|
// Add spacing after file stats, before diff content
|
|
if (total_read < info_size - 4) {
|
|
strcat(commit_info, "\n\n");
|
|
total_read += 2;
|
|
}
|
|
|
|
// Read all the diff content (this includes diff --git headers, @@ hunks, and
|
|
// +/- lines)
|
|
while (fgets(buffer, sizeof(buffer), fp) != NULL &&
|
|
total_read < info_size - 1) {
|
|
size_t line_len = strlen(buffer);
|
|
if (total_read + line_len < info_size - 1) {
|
|
strcpy(commit_info + total_read, buffer);
|
|
total_read += line_len;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
commit_info[total_read] = '\0';
|
|
pclose(fp);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Get stash diff information
|
|
*/
|
|
int get_stash_diff(int stash_index, char *stash_diff, size_t diff_size) {
|
|
if (stash_index < 0 || !stash_diff || diff_size == 0) {
|
|
return 0;
|
|
}
|
|
|
|
char cmd[256];
|
|
snprintf(cmd, sizeof(cmd), "git stash show -p stash@{%d} 2>/dev/null",
|
|
stash_index);
|
|
|
|
FILE *fp = popen(cmd, "r");
|
|
if (!fp) {
|
|
return 0;
|
|
}
|
|
|
|
size_t total_read = 0;
|
|
char buffer[1024];
|
|
|
|
while (fgets(buffer, sizeof(buffer), fp) != NULL &&
|
|
total_read < diff_size - 1) {
|
|
size_t line_len = strlen(buffer);
|
|
if (total_read + line_len < diff_size - 1) {
|
|
strcpy(stash_diff + total_read, buffer);
|
|
total_read += line_len;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
stash_diff[total_read] = '\0';
|
|
pclose(fp);
|
|
|
|
return total_read > 0 ? 1 : 0;
|
|
}
|