diff --git a/git-prompt.c b/git-prompt.c new file mode 100644 index 0000000..e63d871 --- /dev/null +++ b/git-prompt.c @@ -0,0 +1,203 @@ +#include +#include +#include +#include +#include +#include +#if __linux__ +#include +#endif + +#ifdef DEBUG +#define check(CONDITION) \ + if (CONDITION) { \ + fprintf(stderr, "error: %s: %d: %s\n", __FILE__, __LINE__, #CONDITION); \ + exit(0); \ + } +#else +#define check(CONDITION) \ + if (CONDITION) { \ + exit(0); \ + } +#endif + +typedef struct process { + pid_t pid; + FILE* out; +} process_t; + +process_t process_open(char* command) { + int fds[2]; + check(pipe(fds)); + int pid = fork(); + check(pid == -1); + if (pid == 0) { // child process + close(fds[0]); + dup2(fds[1], STDOUT_FILENO); + dup2(STDOUT_FILENO, STDERR_FILENO); + char* argv[] = {"sh", "-c", command, NULL}; + exit(execvp(argv[0], argv)); + } else { // parent process + close(fds[1]); + process_t process = {pid, fdopen(fds[0], "rb")}; + return process; + } +} + +int process_close(process_t process) { + fclose(process.out); + int status; + check(process.pid != waitpid(process.pid, &status, 0)); + if (WIFEXITED(status)) { + return WEXITSTATUS(status); + } + return 0; +} + +char* trim(char* str) { + char* end; + while (isspace((unsigned char)*str)) str++; + if (*str == 0) { + return str; + } + end = str + strlen(str) - 1; + while (end > str && isspace((unsigned char)*end)) { + end--; + } + end[1] = '\0'; + return str; +} + +char* append(char* buffer, int count, ...) { + va_list list; + int i; + va_start(list, count); + for (i = 0; i < count; i++) { + strcat(buffer, va_arg(list, char*)); + } + va_end(list); + return buffer; +} + +char* inttostr(char* buffer, int value) { + sprintf(buffer, "%d", value); + return buffer; +} + +int main() { + // get the current branch name + process_t process = process_open("git symbolic-ref --short HEAD"); + char branch_buf[256] = {}; + fread(branch_buf, 1, sizeof(branch_buf), process.out); + if (process_close(process)) { + // current HEAD is not a symbolic ref + process = process_open("git rev-parse --abbrev-ref HEAD"); + memset(branch_buf, 0, sizeof(branch_buf)); + fread(branch_buf, 1, sizeof(branch_buf), process.out); + check(process_close(process)); + if (strcmp("HEAD", trim(branch_buf)) == 0) { + // get the commit hash + process = process_open("git rev-parse --short HEAD"); + memset(branch_buf, 0, sizeof(branch_buf)); + fread(branch_buf, 1, sizeof(branch_buf), process.out); + check(process_close(process)); + } + } + char* branch = trim(branch_buf); + char prompt[1024] = {}; + append(prompt, 3, " %{%F{66}%}", branch, "%{%f%}"); + + // get the upstream remote if one exists + char command[1024] = {}; + append(command, 3, "git config branch.", branch, ".remote"); + process = process_open(command); + char remote_buf[256] = {}; + fread(remote_buf, 1, sizeof(remote_buf), process.out); + if (process_close(process) == 0) { + char* remote = trim(remote_buf); + // get the number of commits ahead of the remote + memset(command, 0, sizeof(command)); + process = process_open(append(command, 5, + "git rev-list --right-only refs/remotes/", + remote, "/", branch, "...HEAD --count")); + char count[32] = {}; + fread(count, 1, sizeof(count), process.out); + if(process_close(process) == 0 && strcmp("0", trim(count))) { + append(prompt, 2, "↑", trim(count)); + } + + // get the number of commits behind the remote + memset(command, 0, sizeof(command)); + process = process_open(append(command, 5, + "git rev-list --left-only refs/remotes/", + remote, "/", branch, "...HEAD --count")); + memset(count, 0, sizeof(count)); + fread(count, 1, sizeof(count), process.out); + if(process_close(process) == 0 && strcmp("0", trim(count))) { + append(prompt, 2, "↓", trim(count)); + } + } + append(prompt, 1, " "); + + // get the status and parse it + process = process_open("git status --porcelain"); + char status[2048]; + int indexed = 0, modified = 0, deleted = 0, untracked = 0, unmerged = 0; + while (NULL != fgets(status, sizeof(status) - 1, process.out)) { + char X = status[0]; + char Y = status[1]; + + if (X == '?' && Y == '?') { + ++untracked; + } else if ((X == 'A' && (Y == 'A' || Y == 'U')) || + (X == 'D' && (Y == 'D' || Y == 'U')) || + (X == 'U' && (Y == 'A' || Y == 'D' || Y == 'D' || Y == 'U'))) { + ++unmerged; + } else { + switch (X) { + case ' ': + switch (Y) { + case 'M': ++modified; break; + case 'D': ++deleted; break; + } break; + case 'D': ++indexed; + switch (Y) { + case ' ': break; + case 'M': ++modified; break; + } break; + case 'M': case 'A': case 'R': case 'C': ++indexed; + switch (Y) { + case ' ': break; + case 'M': ++modified; break; + case 'D': ++deleted; break; + } break; + } + } + } + check(process_close(process)); + + if (indexed || modified || deleted || unmerged || untracked) { // modified + char int_buf[32]; + if (indexed) { + append(prompt, 3, "%{%F{2}%}*", inttostr(int_buf, indexed), "%{%f%}"); + } + if (modified) { + append(prompt, 3, "%{%F{1}%}+", inttostr(int_buf, modified), "%{%f%}"); + } + if (deleted) { + append(prompt, 3, "%{%F{1}%}-", inttostr(int_buf, deleted), "%{%f%}"); + } + if (unmerged) { + append(prompt, 3, "%{%B%F{1}%}×", inttostr(int_buf, unmerged), "%{%f%}"); + } + if (untracked) { + append(prompt, 1, "%{%F{1}%}…%{%f%}"); + } + } else { // clean + append(prompt, 1, "%{%B%F{2}%}✓%{%f%b%}"); + } + + // print the prompt + puts(prompt); + return 0; +} diff --git a/prompt_fresh_setup b/prompt_fresh_setup index 7bc4666..2f63781 100644 --- a/prompt_fresh_setup +++ b/prompt_fresh_setup @@ -1,17 +1,18 @@ prompt_fresh_help() { echo "\ options: - -a enable almostontop like behaviour" + -a enable almostontop like behaviour + -r recompile git-prompt executable" } prompt_fresh_setup() { - fresh_compile_git_prompt - # Parse options local almostontop=0 - while getopts 'a' opt; do + local recompile=0 + while getopts 'ar' opt; do case $opt in a) local almostontop=1 ;; + r) local recompile=1 ;; *) prompt -h fresh; return 1 ;; esac done @@ -31,6 +32,9 @@ prompt_fresh_setup() { add-zsh-hook -d preexec fresh_almostontop fi + [ $recompile -eq 1 ] && prompt_cleanup + fresh_compile_git_prompt + if [ "$USER" = "root" ]; then local user="%{%F{1}%}%n%{%f%}" else @@ -50,7 +54,7 @@ prompt_fresh_setup() { } prompt_cleanup() { - if [ -f ~/.cache/zsh/git-prompt ]; then rm ~/.cache/zsh/git-prompt; fi + [ -f ~/.cache/zsh/git-prompt ] && rm ~/.cache/zsh/git-prompt } fresh_line_one() { @@ -64,58 +68,7 @@ fresh_line_one() { local directory="%{%F{37}%}%~%{%f%}" # Check we are in a git repository - if `git rev-parse --git-dir > /dev/null 2>&1`; then - # Get git branch name & exit early if not found - local branch="`git symbolic-ref HEAD 2> /dev/null`" - if [ "" = "$branch" ]; then - local branch="`git rev-parse --abbrev-ref HEAD`" - if [ "HEAD" = "$branch" ]; then - local branch="`git rev-parse --short $branch`" - fi - fi - - if [ "$branch" != "" ]; then - local branch=${branch#refs/heads/} - # Get the number of commits ahead/behind from the remote branch - local remote="`git config branch.$branch.remote 2> /dev/null`" - if [ "" != "remote" ]; then - local branches="refs/remotes/$remote/$branch...HEAD" - local nahead=`git rev-list --right-only $branches --count 2> /dev/null` - if [ "0" -ne "$nahead" ]; then local ahead="↑$nahead"; fi - local nbehind=`git rev-list --left-only $branches --count 2> /dev/null` - if [ "0" -ne "$nbehind" ]; then local behind="↓$nbehind"; fi - fi - - # Construct the git prompt - local git=" %{%F{66}%}$branch%{%f%}$ahead$behind " - - if [ -f ~/.cache/zsh/git-prompt ]; then - # Get the change counts from git-prompt - local counts=(`~/.cache/zsh/git-prompt`) - - # Parse the results from git-prompt - if [ 0 -eq ${#counts[@]} ]; then # clean - local git="$git%{%B%F{2}%}✓%{%f%b%}"; - else - if [ 0 != $counts[1] ]; then # indexed - local git="$git%{%F{2}%}*$counts[1]%{%f%}" - fi - if [ 0 != $counts[2] ]; then # modified - local git="$git%{%F{1}%}+$counts[2]%{%f%}"; - fi - if [ 0 != $counts[3] ]; then # deleted - local git="$git%{%F{1}%}-$counts[3]%{%f%}"; - fi - if [ 0 != $counts[4] ]; then # unmerged - local git="$git%{%B%F{1}%}×$counts[4]%{%f%b%}"; - fi - if [ 0 != $counts[5] ]; then # untracked - local git="$git%{%F{1}%}…%{%f%}"; - fi - fi - fi - fi - fi + local git=`~/.cache/zsh/git-prompt` # If the last command failed, display its error code at the right if [[ $exit_code -ne 0 ]]; then @@ -145,62 +98,11 @@ fresh_almostontop() { fresh_compile_git_prompt() { # Compile a simple C program which parses the output of `git status # --procelain` to greatly decrease the time taken to draw the prompt - local cache=~/.cache/zsh; [ ! -d $cache ] && mkdir -p $cache - if [ ! -f $cache/git-prompt ]; then - cc -x c -std=gnu99 -O3 -DNDEBUG -o $cache/git-prompt - 2> /dev/null << EOF -#include -#include - -int main() { - FILE *file = popen("git status --porcelain", "r"); - if (NULL == file) { return 0; } - - char status[2048]; - int indexed = 0, modified = 0, deleted = 0, untracked = 0, unmerged = 0; - - while (NULL != fgets(status, sizeof(status) - 1, file)) { - char X = status[0]; - char Y = status[1]; - - if (X == '?' && Y == '?') { - ++untracked; - } else if ((X == 'A' && (Y == 'A' || Y == 'U')) || - (X == 'D' && (Y == 'D' || Y == 'U')) || - (X == 'U' && (Y == 'A' || Y == 'D' || Y == 'D' || Y == 'U'))) { - ++unmerged; - } else { - switch (X) { - case ' ': - switch (Y) { - case 'M': ++modified; break; - case 'D': ++deleted; break; - } break; - case 'D': ++indexed; - switch (Y) { - case ' ': break; - case 'M': ++modified; break; - } break; - case 'M': case 'A': case 'R': case 'C': ++indexed; - switch (Y) { - case ' ': break; - case 'M': ++modified; break; - case 'D': ++deleted; break; - } break; - } - } - } - - pclose(file); - - if (indexed || modified || deleted || unmerged || untracked) { - printf("%d %d %d %d %d", indexed, modified, deleted, unmerged, untracked); - } - - return 0; -} -EOF + [ ! -d ~/.cache/zsh ] && mkdir -p ~/.cache/zsh + if [ ! -f ~/.cache/zsh/git-prompt ]; then + cc -std=gnu99 -O3 -DNDEBUG ~/.config/zsh/git-prompt.c -o ~/.cache/zsh/git-prompt if [ $? -ne 0 ]; then - echo "git prompt disabled, are the system development headers installed?" + echo "git-prompt was not compiled, is a C99 toolchain installed?" fi fi }