#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <syslog.h>
#include <unistd.h>
#include "config.h"
#include "git.h"
#include "linked_list.h"
#include "macros.h"
#include "misc.h"
int find_ignore_file(const char *path) {
DIR *root = opendir(path);
struct dirent entry, *result;
while (readdir_r(root, &entry, &result) == 0 && result != NULL) {
if (entry.d_type == DT_REG && strcasecmp(entry.d_name, "pullreqd-ignore") == 0) {
return 1;
}
}
return 0;
}
void cleanup_git_repo(git_repo *repo) {
if (repo->name != NULL) {
free(repo->name);
}
if (repo->repo != NULL) {
git_repository_free(repo->repo);
}
}
void cleanup_git_repos(git_repo **repos) {
for (int i = 0; repos[i] != NULL; i++) {
cleanup_git_repo(repos[i]);
repos[i] = NULL;
}
free(repos);
}
void cleanup_git(git_repo **repos) {
cleanup_git_repos(repos);
git_libgit2_shutdown();
}
file *create_file(const char *root, const char *filename) {
const size_t path_len = format_len("%s/%s", root, filename);
char *path = calloc(path_len+1, sizeof(char));
long buf_size = 0;
file *f = calloc(1, sizeof(file));
sprintf(path, "%s/%s", root, filename);
f->name = (char *)filename;
f->buf = read_file(path, &buf_size);
return f;
}
void free_file(file *file) {
if (file->name != NULL) {
free(file->name);
}
if (file->buf != NULL) {
free(file->buf);
}
}
void free_files(file **files) {
for (int i = 0; files[i] != NULL; i++) {
free_file(files[i]);
files[i] = NULL;
}
free(files);
}
static void free_git_branch(git_branch *branch) {
if (branch->name != NULL) {
free(branch->name);
branch->name = NULL;
}
if (branch->repo != NULL) {
free(branch->repo);
branch->repo = NULL;
}
}
static void free_comment(comment *comment) {
if (comment->author != NULL) {
free(comment->author);
comment->author = NULL;
}
if (comment->desc != NULL) {
free(comment->desc);
comment->desc = NULL;
}
comment->reply = NULL;
}
static void free_comments(comment **comments) {
for (int i = 0; comments[i] != NULL; ++i) {
free_comment(comments[i]);
comments[i] = NULL;
}
free(comments);
}
void cleanup_pull_request(pull_request *pr) {
if (pr != NULL) {
if (pr->title != NULL) {
free(pr->title);
pr->title = NULL;
}
if (pr->desc != NULL) {
free(pr->desc);
pr->desc = NULL;
}
if (pr->author != NULL) {
free(pr->author);
pr->author = NULL;
}
if (pr->comments != NULL) {
free_comments(pr->comments);
pr->comments = NULL;
}
if (pr->merge_branch != NULL) {
free(pr->merge_branch);
pr->merge_branch = NULL;
}
if (!pr->pr_type) {
if (pr->patches != NULL) {
free_files(pr->patches);
pr->patches = NULL;
}
} else {
if (pr->branch != NULL) {
free_git_branch(pr->branch);
pr->branch = NULL;
}
}
free(pr);
}
}
static linked_list *parse_dsv_str(const char *str, const char *delm) {
linked_list *tail = NULL;
for (; !is_empty(str); str = find_delm((char *)str, delm, 1)) {
size_t tok_len = strcspn(str, delm);
char *tok = calloc(tok_len+1, sizeof(char));
memcpy(tok, str, tok_len);
tail = add_node(&tail, tok);
}
return tail;
}
int parse_remote_branch(void *ctx, void *ret, const keyword *key, keyword_val val) {
if (key->type != TYPE_STRING) {
if (key->offsets[0] == offsetof(pull_request, branch)) {
if (key->offsets[1] == offsetof(git_branch, name) || key->offsets[1] == offsetof(git_branch, repo)) {
pull_request *pr = (pull_request *)ret;
if (pr->branch == NULL) {
pr->branch = calloc(1, sizeof(git_branch));
}
return 0;
}
}
}
return -1;
}
int parse_patch_list(void *ctx, void *ret, const keyword *key, keyword_val val) {
if (key->type == TYPE_STRING) {
const char *root = (const char *)ctx;
pull_request *pr = (pull_request *)ret;
if (!is_empty(val.str)) {
linked_list *patch_list = parse_dsv_str(val.str, " \t\v");
int num_patches = linked_list_size(patch_list);
pr->patches = calloc(num_patches+1, sizeof(file *));
pr->patches[num_patches--] = NULL;
for (linked_list *patch = patch_list; patch != NULL; patch = patch->prev, --num_patches) {
char *patch_file = create_num_str((const char *)patch->data, num_patches);
char *sanitized_patch_file = sanitize_str(patch_file);
pr->patches[num_patches] = create_file(root, patch_file);
pr->patches[num_patches]->name = (char *)patch->data;
free(patch_file);
free(sanitized_patch_file);
}
cleanup_linked_list(patch_list);
return 1;
}
}
return -1;
}
int parse_comment_reply(void *ctx, void *ret, const keyword *key, keyword_val val) {
linked_list *comment_list = *(linked_list **)ctx;
comment *comm = (comment *)ret;
if (comm->reply != NULL) {
for (linked_list *node = get_tail(comment_list); node != NULL; node = node->prev) {
comment *reply = (comment *)node->data;
if (comm->reply == reply) {
return 1;
}
}
}
if (key->type == TYPE_INT) {
int found_reply = 0;
for (linked_list *node = get_tail(comment_list); node != NULL; node = node->prev) {
comment *reply = (comment *)node->data;
if (reply->id == val.i) {
if (comm->reply != NULL) {
free(comm->reply);
}
comm->reply = reply;
found_reply = 1;
break;
}
}
if (found_reply) {
return 1;
}
}
if (comm->reply == NULL) {
comm->reply = calloc(1, sizeof(comment));
}
switch (key->type) {
case TYPE_INT : comm->reply->id = val.i; break;
case TYPE_STRING: comm->reply->author = val.str; break;
case TYPE_TIME : comm->reply->date = val.t; break;
default : return -1; break;
}
return 1;
}
static int is_end_of_comment(void **ret, void *ctx, char *buf) {
if (strspn(buf, "\n") >= 1) {
linked_list **comment_list = (linked_list **)ctx;
if (comment_list != NULL) {
if (*comment_list == NULL || (*comment_list)->data == NULL) {
add_node(comment_list, *ret);
}
*ret = calloc(1, sizeof(comment));
return 1;
} else {
return -1;
}
}
return 0;
}
static int parse_comments_file(comment ***comments, char *buf) {
if (comments == NULL) {
return 9;
} else {
linked_list *comment_list = NULL;
int ret = parse_key_value_file(calloc(1, sizeof(comment)), &comment_list, comment_keywords, buf, ":", is_end_of_comment);
*comments = (comment **)linked_list_to_array(comment_list);
cleanup_linked_list(comment_list);
return ret;
}
}
static int parse_info_file(pull_request *pr, char *buf, const char *root) {
return parse_key_value_file(pr, (void *)root, info_keywords, buf, ":", NULL);
}
static int parse_comments_file_path(comment ***comments, const char *root) {
file *f = create_file(root, "comments");
int ret = parse_comments_file(comments, f->buf);
f->name = NULL;
free_file(f);
return ret;
}
static int parse_info_file_path(pull_request *pr, const char *root) {
file *f = create_file(root, "info");
int ret = parse_info_file(pr, f->buf, root);
f->name = NULL;
free_file(f);
return ret;
}
pull_request *get_pull_request(index_t *idx, const char *root, const char *repo) {
if (is_valid_index(idx)) {
char *pr_dir;
char *idx_str = index_to_str(idx);
char *pr_root = (!is_empty(repo)) ? dir_path_name(root, (char *)repo) : (char *)root;
if (index_path_exists(idx, (const char *)pr_root, &pr_dir) >= 0) {
pull_request *pr = calloc(1, sizeof(pull_request));
if (!is_empty(repo)) {
free(pr_root);
}
log(LOG_NOTICE, "Found PR #%s with path \"%s\".", idx_str, pr_dir);
if (parse_info_file_path(pr, pr_dir) <= 1) {
log(LOG_NOTICE, "Successfully parsed info file of PR #%s.", idx_str);
if (parse_comments_file_path(&pr->comments, pr_dir) <= 1) {
log(LOG_NOTICE, "Successfully parsed comments file of PR #%s.", idx_str);
} else {
log(LOG_WARNING, "Failed to parse comments file of PR #%s.", idx_str);
}
free(pr_dir);
return pr;
} else {
log(LOG_ERR, "Failed to parse info file of PR #%s.", idx_str);
free(pr);
free(pr_dir);
return NULL;
}
} else {
log(LOG_ERR, "No PR #%s found.", idx_str);
if (!is_empty(repo)) {
free(pr_root);
}
free(idx_str);
return NULL;
}
} else {
return NULL;
}
}
int get_pr_type(void *ctx, void *data, const keyword *key, keyword_val *val) {
pull_request *pr = (pull_request *)data;
if (key->type == TYPE_INT && pr != NULL) {
val->i = pr->pr_type;
return 1;
} else {
return -1;
}
}
int has_remote_branch(void *ctx, void *data, const keyword *key, keyword_val *val) {
pull_request *pr = (pull_request *)data;
return (pr->pr_type) ? 0 : -1;
}
int get_patch_list(void *ctx, void *data, const keyword *key, keyword_val *val) {
pull_request *pr = (pull_request *)data;
if (key->type == TYPE_STRING && !pr->pr_type && pr->patches != NULL) {
int len = 0;
char *tmp;
for (int i = 0; pr->patches[i] != NULL; i++) {
len += format_len("% *s", (int)strlen(pr->patches[i]->name)+(i > 0), pr->patches[i]->name);
}
val->str = calloc(len+1, sizeof(char));
tmp = val->str;
for (int i = 0; pr->patches[i] != NULL; i++) {
tmp += sprintf(tmp, "%*s", (int)strlen(pr->patches[i]->name)+(i > 0), pr->patches[i]->name);
}
return 2;
} else {
return -1;
}
}
int create_info_file(pull_request *pr, const char *pr_root) {
char *filename;
char *file_buf;
FILE *fp;
if (pr == NULL) {
log(LOG_ERR,"Pull request is NULL.");
return 0;
}
if (pr_root == NULL) {
log(LOG_ERR,"Pull request root is NULL.");
return 0;
}
filename = calloc(format_len("%s/info", pr_root), sizeof(char));
file_buf = create_key_value_file(pr, (void *)pr_root, info_keywords, &(const delimiter){":", 0, 1}, &(const delimiter){"\n", 0, 0});
sprintf(filename, "%s/info", pr_root);
if (access(filename, F_OK) == 0) {
long size = 0;
char *buf = read_file(filename, &size);
log(LOG_NOTICE, "Info file already exists.");
if (buf != NULL) {
if (strcmp(file_buf, buf) == 0) {
log(LOG_NOTICE, "New info file is the same as the existing one.");
free(filename);
free(file_buf);
free(buf);
return 2;
}
free(buf);
} else {
log(LOG_ERR, "Couldn't open existing info file.");
return 0;
}
}
fp = fopen(filename, "w");
if (fp == NULL) {
log(LOG_ERR, "Failed to open info file.");
return 0;
}
fwrite(file_buf, sizeof(char), strlen(file_buf), fp);
fclose(fp);
free(filename);
free(file_buf);
return 1;
}
int get_comment_reply(void *ctx, void *data, const keyword *key, keyword_val *val) {
comment *comm = (comment *)data;
if (comm->reply != NULL) {
switch (key->type) {
case TYPE_INT : val->i = comm->reply->id; break;
case TYPE_STRING: val->str = comm->reply->author; break;
case TYPE_TIME : val->t = comm->reply->date; break;
default : return -1; break;
}
return 1;
} else {
return -1;
}
}
int get_comment_len(comment *comment) {
struct tm tm;
int len = format_len("id: %i\n", comment->id);
localtime_r(&comment->date, &tm);
len += format_len("author: %s\n", comment->author);
len += format_len("date: ") + strftime(NULL, -1, "%a %b %e %H:%M:%S %Y %z\n", &tm);
if (comment->reply != NULL) {
localtime_r(&comment->reply->date, &tm);
len += format_len("reply-to: %i\n", comment->reply->id);
len += format_len("reply-author: %s\n", comment->reply->author);
len += format_len("reply-date: ") + strftime(NULL, -1, "%a %b %e %H:%M:%S %Y %z\n", &tm);
}
len += format_len("description: %s\n\n", comment->desc);
return len;
}
int add_comment(comment *comment, const char *pr_root) {
int len;
int buf_len;
int j = 0;
char *filename;
char *file_buf;
FILE *fp;
struct tm tm;
if (comment == NULL) {
log(LOG_ERR, "Comment is NULL.");
return 0;
}
if (pr_root == NULL) {
log(LOG_ERR,"Pull request root is NULL.");
return 0;
}
localtime_r(&comment->date, &tm);
len = strlen(pr_root) + strlen("/comments");
buf_len = get_comment_len(comment);
filename = calloc(len+1, sizeof(char));
file_buf = calloc(buf_len+1, sizeof(char));
j = sprintf(file_buf, "id: %i\n", comment->id);
j += sprintf(file_buf+j, "author: %s\n", comment->author);
j += sprintf(file_buf+j, "date: ");
j += strftime(file_buf+j, -1, "%a %b %e %H:%M:%S %Y %z\n", &tm);
if (comment->reply != NULL) {
localtime_r(&comment->reply->date, &tm);
j += sprintf(file_buf+j, "reply-to: %i\n", comment->reply->id);
j += sprintf(file_buf+j, "reply-author: %s\n", comment->reply->author);
j += sprintf(file_buf+j, "reply-date: ");
j += strftime(file_buf+j, -1, "%a %b %e %H:%M:%S %Y %z\n", &tm);
}
j += sprintf(file_buf+j, "description: %s\n\n", comment->desc);
sprintf(filename, "%s/comments", pr_root);
fp = fopen(filename, "a");
if (fp == NULL) {
log(LOG_ERR, "Failed to open comments file.");
return 0;
}
fwrite(file_buf, sizeof(char), buf_len, fp);
fclose(fp);
return 1;
}
file **get_branch_commits(git_branch *br) {
return NULL;
}
git_repo *get_repo(git_repo **repos, const char *name) {
if (!is_empty(name)) {
for (int i = 0; repos[i] != NULL; ++i) {
if (!is_empty(repos[i]->name) && !strcmp(repos[i]->name, name)) {
return repos[i];
}
}
}
return NULL;
}
int create_pull_request_branch(pull_request *pr, index_t *idx, git_repo *repo) {
int ret = 0;
if (pr == NULL) {
log(LOG_ERR, "Pull Request is NULL.");
return -1;
} else {
char *pr_branch_index = make_index_path("pr", idx, 0);
if (is_empty(pr_branch_index)) {
log(LOG_ERR, "Empty PR branch index.");
return -1;
} else {
char *pr_branch = pr_branch_index;
if (pr->pr_type) {
pr_branch = dir_path_name((const char *)pr_branch_index, pr->branch->name);
free(pr_branch_index);
}
if (repo != NULL && repo->repo != NULL) {
if (pr->pr_type) {
git_branch *branch = pr->branch;
if (!is_empty(branch->repo)) {
git_remote *remote;
if (!git_remote_create_anonymous(&remote, repo->repo, branch->repo)) {
char *branch_name = branch->name;
char *refspec;
const git_strarray refspecs = {&refspec, 1};
if (is_empty(branch->name)) {
git_buf default_branch = {0};
log(LOG_WARNING, "Empty remote repo branch name, using default remote branch instead.");
if (!git_remote_default_branch(&default_branch, remote)) {
branch_name = default_branch.ptr;
} else {
log(LOG_ERR, "Failed to get default branch of remote repo \"%s\".", branch->repo);
free(pr_branch);
return -1;
}
}
refspec = calloc(format_len("%s:%s", branch_name, pr_branch)+1, sizeof(char));
sprintf(refspec, "%s:%s", branch_name, pr_branch);
if (!git_remote_fetch(remote, &refspecs, NULL, NULL)) {
log(LOG_NOTICE, "Successfully fetched branch \"%s\" of remote repo \"%s\" into branch \"%s\".", branch_name, branch->repo, pr_branch);
ret = 0;
} else {
log(LOG_ERR, "Failed to fetch remote repo \"%s\".", branch->repo);
ret = -1;
}
if (branch_name != branch->name) {
free(branch_name);
}
free(refspec);
free(pr_branch);
return ret;
} else {
log(LOG_ERR, "Failed to create anonymous remote for remote repo \"%s\".", branch->repo);
free(pr_branch);
return -1;
}
}
}
} else {
log(LOG_ERR, "Git repo is NULL.");
free(pr_branch);
return -1;
}
}
}
}
int create_pull_request_dir(pull_request *pr, index_t *idx, const char *root, const char *repo) {
int ret = 0;
struct stat st;
file **commits;
char *pr_dir;
char *pr_root = (!is_empty(repo)) ? dir_path_name(root, (char *)repo) : (char *)root;
char *reason;
if (pr == NULL) {
log(LOG_ERR, "Pull Request is NULL.");
return -1;
}
pr_dir = make_index_path((const char *)pr_root, idx, 0);
if (!is_empty(repo)) {
free(pr_root);
}
if (pr_dir == NULL) {
if (is_empty(pr->title)) {
log(LOG_ERR, "Empty PR title.");
return -1;
}
pr_dir = sanitized_dir_path_name(root, pr->title);
}
switch (is_dir(pr_dir)) {
case 1 : break;
case 0 :
log(LOG_ERR, "PR root \"%s\" exists, but isn't a directory.", pr_dir);
free(pr_dir);
return -1;
break;
default : mkdirp(pr_dir, 0755); break;
}
if (!create_info_file(pr, pr_dir)) {
log(LOG_ERR, "Failed to create info file.");
return -1;
}
commits = (pr->pr_type) ? get_branch_commits(pr->branch) : pr->patches;
if (commits != NULL) {
for (int i = 0; commits[i] != NULL; i++) {
FILE *fp;
char *filename = create_num_str(commits[i]->name, i);
char *file = sanitized_dir_path_name(pr_dir, filename);
if (access(file, F_OK) == 0) {
long size = 0;
char *buf = read_file(file, &size);
log(LOG_NOTICE, "Patch file %s already exists.", filename);
if (buf != NULL) {
if (strcmp(commits[i]->buf, buf) == 0) {
log(LOG_NOTICE, "Patch file %s is the same as the new one.", filename);
free(buf);
free(filename);
free(file);
continue;
}
free(buf);
} else {
log(LOG_WARNING, "Couldn't open patch file %s.", filename);
free(filename);
free(file);
continue;
}
}
fp = fopen(file, "w");
fwrite(commits[i]->buf, sizeof(char), strlen(commits[i]->buf), fp);
fclose(fp);
free(filename);
free(file);
}
if (commits != NULL && pr->pr_type) {
free_files(commits);
}
} else {
log(LOG_WARNING, "Patch file array is NULL.");
}
free(pr_dir);
return ret;
}
git_repo **init_git(config *cfg) {
git_repo **repos = NULL;
linked_list *repo_list = NULL;
DIR *root = opendir(cfg->git_root);
struct dirent entry, *result;
log(LOG_INFO, "Initializing libgit2.");
int ret = git_libgit2_init();
log(LOG_INFO, "Searching \"%s\" for repositories.", cfg->git_root);
while (readdir_r(root, &entry, &result) == 0 && result != NULL) {
if (entry.d_type == DT_DIR) {
if (strcmp(entry.d_name, ".") && strcmp(entry.d_name, "..")) {
char *repo_dir = calloc(format_len("%s/%s", cfg->git_root, entry.d_name), sizeof(char));
sprintf(repo_dir, "%s/%s", cfg->git_root, entry.d_name);
if (!find_ignore_file(repo_dir)) {
git_repo *repo = calloc(1, sizeof(git_repo));
repo->name = make_str(entry.d_name);
if (git_repository_open(&repo->repo, repo_dir)) {
log(LOG_ERR, "Failed to open git repository %s, ignoring.", entry.d_name);
} else {
log(LOG_INFO, "Successfully opened git repository %s", entry.d_name);
repo_list = add_node(&repo_list, repo);
}
}
free(repo_dir);
}
}
}
if (repo_list != NULL) {
log(LOG_INFO, "Found, and opened %i repositories.", linked_list_size(repo_list));
repos = (git_repo **)linked_list_to_array(repo_list);
cleanup_linked_list(repo_list);
} else {
log(LOG_ERR, "Couldn't find, and/or open any repositories.");
}
return repos;
}