mirror of
https://github.com/marcusDenslow/FERRUM.git
synced 2026-10-25 14:38:50 +01:00
if you hover over a branc now you can see the commit history on that branch. it will display the hash author date title and message associated with that commit. very good very nice!!!!
594 lines
13 KiB
C
594 lines
13 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;
|
|
}
|
|
|
|
int get_branch_commits(const char *branch_name, char commits[][2048], int max_commits) {
|
|
if (!branch_name || !commits || max_commits <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
char cmd[1024];
|
|
snprintf(cmd, sizeof(cmd),
|
|
"git log %s --format=\"commit %%H%%d%%nAuthor: %%an <%%ae>%%nDate: %%ar%%n%%n %%s%%n%%n%%b%%n---END-COMMIT---\" -%d 2>/dev/null",
|
|
branch_name, max_commits);
|
|
|
|
FILE *fp = popen(cmd, "r");
|
|
if (!fp) {
|
|
return 0;
|
|
}
|
|
|
|
int count = 0;
|
|
char buffer[8192];
|
|
char current_commit[4096] = "";
|
|
|
|
while (fgets(buffer, sizeof(buffer), fp) != NULL && count < max_commits) {
|
|
// Check for commit delimiter
|
|
if (strstr(buffer, "---END-COMMIT---")) {
|
|
if (strlen(current_commit) > 0) {
|
|
strncpy(commits[count], current_commit, 2047);
|
|
commits[count][2047] = '\0';
|
|
count++;
|
|
current_commit[0] = '\0';
|
|
}
|
|
} else {
|
|
// Accumulate commit content
|
|
if (strlen(current_commit) + strlen(buffer) < sizeof(current_commit) - 1) {
|
|
strcat(current_commit, buffer);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle last commit if it doesn't end with delimiter
|
|
if (strlen(current_commit) > 0 && count < max_commits) {
|
|
strncpy(commits[count], current_commit, 2047);
|
|
commits[count][2047] = '\0';
|
|
count++;
|
|
}
|
|
|
|
pclose(fp);
|
|
return count;
|
|
}
|