|
#include <stdio.h> |
|
#include <stdlib.h> |
|
#include <string.h> |
|
#include <ctype.h> |
|
#include <sys/stat.h> |
|
#include <time.h> |
|
#include <dirent.h> |
|
#include <limits.h> // For PATH_MAX |
|
#include <unistd.h> |
|
|
|
// Define the maximum memory size to use (in bytes). Default is 1GB. |
|
#define MAX_MEMORY_LIMIT (1 * 1024 * 1024 * 1024) // 1GB |
|
|
|
// Structure to store worktree path and its corresponding age |
|
typedef struct |
|
{ |
|
char worktree_path[PATH_MAX]; |
|
long long age; |
|
long long mtime; |
|
} WorktreeInfo; |
|
|
|
// Sorting options |
|
typedef enum |
|
{ |
|
SORT_BY_AGE_ASC, |
|
SORT_BY_AGE_DESC, |
|
SORT_BY_PATH_ASC, |
|
SORT_BY_PATH_DESC |
|
} SortOption; |
|
|
|
char *read_file_in_chunks(const char *filename, size_t *out_size, size_t chunk_size) |
|
{ |
|
FILE *file = fopen(filename, "r"); |
|
|
|
if (file == NULL) |
|
{ |
|
perror("Error opening file"); |
|
return NULL; |
|
} |
|
|
|
// Allocate an initial buffer for reading |
|
size_t buffer_size = chunk_size; |
|
char *buffer = (char *)malloc(buffer_size); |
|
if (buffer == NULL) |
|
{ |
|
perror("Error allocating memory"); |
|
fclose(file); |
|
return NULL; |
|
} |
|
|
|
size_t total_bytes_read = 0; |
|
size_t bytes_read; |
|
|
|
while ((bytes_read = fread(buffer + total_bytes_read, 1, chunk_size, file)) > 0) |
|
{ |
|
total_bytes_read += bytes_read; |
|
|
|
// If the buffer is full, realloc to expand it |
|
if (total_bytes_read + chunk_size > buffer_size) |
|
{ |
|
buffer_size *= 2; |
|
buffer = (char *)realloc(buffer, buffer_size); |
|
if (buffer == NULL) |
|
{ |
|
perror("Error reallocating memory"); |
|
fclose(file); |
|
return NULL; |
|
} |
|
} |
|
} |
|
|
|
// Null-terminate the string and update out_size |
|
buffer[total_bytes_read] = '\0'; |
|
*out_size = total_bytes_read; |
|
|
|
fclose(file); |
|
return buffer; // Return the dynamically allocated buffer |
|
} |
|
|
|
char *remove_git_suffix(char *path) |
|
{ |
|
if (path == NULL) |
|
{ |
|
return path; |
|
} |
|
|
|
// Find the length of the path |
|
size_t len = strlen(path); |
|
|
|
// Check if the path ends with "/.git" |
|
if (len > 5 && strcmp(path + len - 5, "/.git") == 0) |
|
{ |
|
// Null-terminate the string before "/.git" |
|
path[len - 5] = '\0'; |
|
} |
|
|
|
return path; |
|
} |
|
|
|
char *trim_whitespace(char *str) |
|
{ |
|
if (str == NULL) |
|
{ |
|
return NULL; |
|
} |
|
|
|
// Trim leading whitespace |
|
while (isspace((unsigned char)*str)) |
|
{ |
|
str++; |
|
} |
|
|
|
// If the string is all whitespace, return an empty string |
|
if (*str == '\0') |
|
{ |
|
return str; |
|
} |
|
|
|
// Trim trailing whitespace |
|
char *end = str + strlen(str) - 1; |
|
while (end > str && isspace((unsigned char)*end)) |
|
{ |
|
end--; |
|
} |
|
|
|
// Null-terminate the string |
|
*(end + 1) = '\0'; |
|
|
|
return str; |
|
} |
|
|
|
// Function to display the help message |
|
void show_help(const char *program_name) |
|
{ |
|
printf("Usage: %s <directory> [options]\n", program_name); |
|
printf("\n"); |
|
printf("This program scans a directory for Git repositories and lists their worktrees with their age.\n"); |
|
printf("The output will show the time difference in terms of years, months, weeks, days, hours, minutes, and seconds.\n"); |
|
printf("\n"); |
|
printf("Example: %s /path/to/repo\n", program_name); |
|
printf("Output: 1 y 2 mo 3 w 4 d 5 h ago /path/to/worktree\n"); |
|
printf("\n"); |
|
printf("Options:\n"); |
|
printf(" -h, --help Show this help message and exit\n"); |
|
printf(" --age-asc Sort by age (ascending)\n"); |
|
printf(" --age-desc Sort by age (descending)\n"); |
|
printf(" --path-asc Sort by path (alphabetical ascending)\n"); |
|
printf(" --path-desc Sort by path (alphabetical descending)\n"); |
|
printf(" -n <num> Limit the number of rows displayed\n"); |
|
printf(" --age-hidden Hide the age in the output\n"); |
|
printf(" --path-hidden Hide the path in the output\n"); |
|
printf("\n"); |
|
} |
|
|
|
// Function to format time difference into a human-readable string with up to 2 factors |
|
char *format_age(long long seconds, char *buffer) |
|
{ |
|
long long years = seconds / 31536000; |
|
seconds %= 31536000; |
|
long long months = seconds / 2592000; |
|
seconds %= 2592000; |
|
long long weeks = seconds / 604800; |
|
seconds %= 604800; |
|
long long days = seconds / 86400; |
|
seconds %= 86400; |
|
long long hours = seconds / 3600; |
|
seconds %= 3600; |
|
long long minutes = seconds / 60; |
|
long long remaining_seconds = seconds % 60; |
|
|
|
buffer[0] = '\0'; // Clear the buffer |
|
|
|
// Counter for the number of factors added |
|
int factor_count = 0; |
|
|
|
// Check if any of the time units are greater than zero and add them to the result |
|
if (years > 0) |
|
{ |
|
sprintf(buffer + strlen(buffer), "%lldy ", years); |
|
factor_count++; |
|
} |
|
if (months > 0) |
|
{ |
|
sprintf(buffer + strlen(buffer), "%lldmo ", months); |
|
} |
|
if (weeks > 0 && months == 0) |
|
{ |
|
sprintf(buffer + strlen(buffer), "%lldw ", weeks); |
|
} |
|
if (days > 0) |
|
{ |
|
sprintf(buffer + strlen(buffer), "%lldd ", days); |
|
} |
|
if (hours > 0) |
|
{ |
|
sprintf(buffer + strlen(buffer), "%lldh ", hours); |
|
} |
|
if (minutes > 0) |
|
{ |
|
sprintf(buffer + strlen(buffer), "%lldm ", minutes); |
|
} |
|
if (remaining_seconds > 0) |
|
{ |
|
sprintf(buffer + strlen(buffer), "%llds ", remaining_seconds); |
|
} |
|
|
|
// Remove trailing space and add "ago" at the end |
|
if (strlen(buffer) > 0 && buffer[strlen(buffer) - 1] == ' ') |
|
{ |
|
buffer[strlen(buffer) - 1] = '\0'; // Remove the trailing space |
|
} |
|
strcat(buffer, " ago"); |
|
|
|
return buffer; |
|
} |
|
|
|
char *format_mtime_elapsed(time_t mtime, char *buffer) |
|
{ |
|
time_t current_time = time(NULL); |
|
if (current_time == (time_t)-1) |
|
{ |
|
perror("Error getting current time"); |
|
return NULL; |
|
} |
|
|
|
long long seconds = (long long)(current_time - mtime); |
|
|
|
return format_age(seconds, buffer); |
|
} |
|
|
|
// Function to get the creation time of a worktree |
|
long long get_worktree_age(const char *worktree_path) |
|
{ |
|
struct stat worktree_stat; |
|
if (stat(worktree_path, &worktree_stat) != 0) |
|
{ |
|
perror("Error getting file stat"); |
|
return -1; |
|
} |
|
|
|
time_t current_time = time(NULL); |
|
if (current_time == (time_t)-1) |
|
{ |
|
perror("Error getting current time"); |
|
return -1; |
|
} |
|
|
|
return (long long)(current_time - worktree_stat.st_mtime); // Time in seconds |
|
} |
|
|
|
long long get_last_updated_time(const char *worktree_path) |
|
{ |
|
struct stat worktree_stat; |
|
if (stat(worktree_path, &worktree_stat) != 0) |
|
{ |
|
perror("Error getting file stat"); |
|
return -1; |
|
} |
|
|
|
return (long long)worktree_stat.st_mtime; // Last modification time in seconds |
|
} |
|
|
|
// Comparison function to sort worktree information by age (ascending) |
|
int compare_by_age_asc(const void *a, const void *b) |
|
{ |
|
WorktreeInfo *worktree_a = (WorktreeInfo *)a; |
|
WorktreeInfo *worktree_b = (WorktreeInfo *)b; |
|
|
|
if (worktree_a->age < worktree_b->age) |
|
return -1; |
|
if (worktree_a->age > worktree_b->age) |
|
return 1; |
|
return 0; |
|
} |
|
|
|
// Comparison function to sort worktree information by age (descending) |
|
int compare_by_age_desc(const void *a, const void *b) |
|
{ |
|
return compare_by_age_asc(b, a); // Invert order |
|
} |
|
|
|
// Comparison function to sort worktree information by path (ascending) |
|
int compare_by_path_asc(const void *a, const void *b) |
|
{ |
|
WorktreeInfo *worktree_a = (WorktreeInfo *)a; |
|
WorktreeInfo *worktree_b = (WorktreeInfo *)b; |
|
|
|
return strcmp(worktree_a->worktree_path, worktree_b->worktree_path); |
|
} |
|
|
|
// Comparison function to sort worktree information by path (descending) |
|
int compare_by_path_desc(const void *a, const void *b) |
|
{ |
|
return compare_by_path_asc(b, a); // Invert order |
|
} |
|
|
|
// Function to scan for Git repositories in a directory and list worktrees |
|
void scan_git_repos(const char *dir, SortOption sort_option, int limit, int hide_age, int hide_path) |
|
{ |
|
struct dirent *entry; |
|
DIR *dp = opendir(dir); |
|
if (dp == NULL) |
|
{ |
|
fprintf(stderr, "Error: Unable to open directory %s\n", dir); |
|
return; |
|
} |
|
|
|
WorktreeInfo *worktrees = malloc(sizeof(WorktreeInfo) * 1024); // Allocate space for up to 1024 worktrees |
|
size_t worktree_count = 0; |
|
|
|
// Traverse through the directory |
|
while ((entry = readdir(dp)) != NULL) |
|
{ |
|
// Skip . and .. |
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) |
|
{ |
|
continue; |
|
} |
|
|
|
char git_dir[PATH_MAX]; |
|
snprintf(git_dir, sizeof(git_dir), "%s/%s/.git", dir, entry->d_name); |
|
struct stat statbuf; |
|
|
|
// Check if it's a Git repository |
|
if (stat(git_dir, &statbuf) == 0 && S_ISDIR(statbuf.st_mode)) |
|
{ |
|
// Check for worktrees in the repository |
|
char worktree_dir[PATH_MAX]; |
|
snprintf(worktree_dir, sizeof(worktree_dir), "%s/%s/.git/worktrees", dir, entry->d_name); |
|
|
|
DIR *worktree_dp = opendir(worktree_dir); |
|
if (worktree_dp) |
|
{ |
|
struct dirent *worktree_entry; |
|
while ((worktree_entry = readdir(worktree_dp)) != NULL) |
|
{ |
|
// Skip . and .. |
|
if (strcmp(worktree_entry->d_name, ".") == 0 || strcmp(worktree_entry->d_name, "..") == 0) |
|
{ |
|
continue; |
|
} |
|
|
|
// Get the full path of the worktree |
|
char worktree_path[PATH_MAX]; |
|
snprintf(worktree_path, sizeof(worktree_path), "%s/%s/.git/worktrees/%s", dir, entry->d_name, worktree_entry->d_name); |
|
|
|
// Get the age of the worktree |
|
long long age = get_worktree_age(worktree_path); |
|
long long mtime = get_last_updated_time(worktree_path); |
|
if (age >= 0) |
|
{ |
|
size_t file_size = 0; |
|
snprintf(worktree_path, sizeof(worktree_path), "%s/gitdir", worktree_path); |
|
char *read_worktree = read_file_in_chunks(worktree_path, &file_size, 1024); // Read the file in 1KB chunks |
|
|
|
strncpy(worktrees[worktree_count].worktree_path, remove_git_suffix(trim_whitespace(read_worktree)), PATH_MAX); |
|
free(read_worktree); |
|
worktrees[worktree_count].age = age; |
|
worktrees[worktree_count].mtime = mtime; |
|
worktree_count++; |
|
|
|
// If the array exceeds the size, reallocate |
|
if (worktree_count >= 1024) |
|
{ |
|
worktrees = realloc(worktrees, sizeof(WorktreeInfo) * (worktree_count + 1024)); |
|
} |
|
} |
|
} |
|
closedir(worktree_dp); |
|
} |
|
} |
|
} |
|
|
|
// Sort based on the chosen option |
|
switch (sort_option) |
|
{ |
|
case SORT_BY_AGE_ASC: |
|
qsort(worktrees, worktree_count, sizeof(WorktreeInfo), compare_by_age_asc); |
|
break; |
|
case SORT_BY_AGE_DESC: |
|
qsort(worktrees, worktree_count, sizeof(WorktreeInfo), compare_by_age_desc); |
|
break; |
|
case SORT_BY_PATH_ASC: |
|
qsort(worktrees, worktree_count, sizeof(WorktreeInfo), compare_by_path_asc); |
|
break; |
|
case SORT_BY_PATH_DESC: |
|
qsort(worktrees, worktree_count, sizeof(WorktreeInfo), compare_by_path_desc); |
|
break; |
|
} |
|
|
|
// Display the sorted worktrees (limit the number of rows) |
|
char age_buffer[100]; |
|
int row_count = 0; |
|
for (size_t i = 0; i < worktree_count; i++) |
|
{ |
|
if (limit > 0 && row_count >= limit) |
|
break; |
|
|
|
if (!hide_age) |
|
{ |
|
printf("%-25s ", format_mtime_elapsed(worktrees[i].mtime, age_buffer)); |
|
} |
|
if (!hide_path) |
|
{ |
|
printf("%-40s\n", worktrees[i].worktree_path); |
|
} |
|
|
|
row_count++; |
|
} |
|
|
|
free(worktrees); // Free the allocated memory |
|
closedir(dp); |
|
} |
|
|
|
// Function to check if a directory exists |
|
int directory_exists(const char *path) |
|
{ |
|
struct stat statbuf; |
|
return (stat(path, &statbuf) == 0 && S_ISDIR(statbuf.st_mode)); |
|
} |
|
|
|
// Main function with enhanced memory handling and directory scanning |
|
int main(int argc, char *argv[]) |
|
{ |
|
// Check if help option is requested |
|
if (argc == 2 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) |
|
{ |
|
show_help(argv[0]); |
|
return 0; |
|
} |
|
|
|
// Validate the number of arguments |
|
if (argc < 2 || argc > 5) |
|
{ |
|
fprintf(stderr, "Error: Invalid number of arguments.\n"); |
|
show_help(argv[0]); |
|
return 1; |
|
} |
|
|
|
// Get the target directory |
|
const char *target_dir = argv[1]; |
|
SortOption sort_option = SORT_BY_AGE_DESC; // Default sorting by age descending |
|
int limit = -1; // No limit by default |
|
int hide_age = 0; |
|
int hide_path = 0; |
|
|
|
// Parse sorting options and other flags |
|
for (int i = 2; i < argc; i++) |
|
{ |
|
if (strcmp(argv[i], "--age-asc") == 0) |
|
{ |
|
sort_option = SORT_BY_AGE_ASC; |
|
} |
|
else if (strcmp(argv[i], "--age-desc") == 0) |
|
{ |
|
sort_option = SORT_BY_AGE_DESC; |
|
} |
|
else if (strcmp(argv[i], "--path-asc") == 0) |
|
{ |
|
sort_option = SORT_BY_PATH_ASC; |
|
} |
|
else if (strcmp(argv[i], "--path-desc") == 0) |
|
{ |
|
sort_option = SORT_BY_PATH_DESC; |
|
} |
|
else if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) |
|
{ |
|
limit = atoi(argv[i + 1]); |
|
i++; // Skip the next argument |
|
} |
|
else if (strcmp(argv[i], "--age-hidden") == 0) |
|
{ |
|
hide_age = 1; |
|
} |
|
else if (strcmp(argv[i], "--path-hidden") == 0) |
|
{ |
|
hide_path = 1; |
|
} |
|
else |
|
{ |
|
fprintf(stderr, "Error: Unknown option %s\n", argv[i]); |
|
show_help(argv[0]); |
|
return 1; |
|
} |
|
} |
|
|
|
// Check if the directory exists |
|
if (!directory_exists(target_dir)) |
|
{ |
|
fprintf(stderr, "Error: %s is not a valid directory.\n", target_dir); |
|
return 1; |
|
} |
|
|
|
scan_git_repos(target_dir, sort_option, limit, hide_age, hide_path); // Start scanning the Git repositories |
|
|
|
return 0; |
|
} |