index a5255cd39de29da90218293fe9239471a860bfdf..8deab13773e0646d6269c71077103e10e79574b8 100644 (file)
--- a/tig.c
+++ b/tig.c
*/
#ifndef VERSION
*/
#ifndef VERSION
-#define VERSION "tig-0.4.git"
+#define VERSION "tig-0.5.git"
#endif
#ifndef DEBUG
#endif
#ifndef DEBUG
#define SIZEOF_STR 1024 /* Default string size. */
#define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
#define SIZEOF_STR 1024 /* Default string size. */
#define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
+#define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
+
+/* Revision graph */
+
+#define REVGRAPH_INIT 'I'
+#define REVGRAPH_MERGE 'M'
+#define REVGRAPH_BRANCH '+'
+#define REVGRAPH_COMMIT '*'
+#define REVGRAPH_LINE '|'
+
#define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
/* This color name can be used to refer to the default term colors. */
#define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
/* This color name can be used to refer to the default term colors. */
#define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
#define TIG_LS_REMOTE \
#define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
#define TIG_LS_REMOTE \
- "git ls-remote . 2>/dev/null"
+ "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
#define TIG_DIFF_CMD \
"git show --root --patch-with-stat --find-copies-harder -B -C %s 2>/dev/null"
#define TIG_DIFF_CMD \
"git show --root --patch-with-stat --find-copies-harder -B -C %s 2>/dev/null"
struct ref {
char *name; /* Ref name; tag or head names are shortened. */
struct ref {
char *name; /* Ref name; tag or head names are shortened. */
- char id[41]; /* Commit SHA1 ID */
+ char id[SIZEOF_REV]; /* Commit SHA1 ID */
unsigned int tag:1; /* Is it a tag? */
unsigned int tag:1; /* Is it a tag? */
+ unsigned int remote:1; /* Is it a remote ref? */
unsigned int next:1; /* For ref lists: are there more refs? */
};
unsigned int next:1; /* For ref lists: are there more refs? */
};
for (i = 1; i < argc; i++) {
char *opt = argv[i];
for (i = 1; i < argc; i++) {
char *opt = argv[i];
+ if (!strcmp(opt, "log") ||
+ !strcmp(opt, "diff") ||
+ !strcmp(opt, "show")) {
+ opt_request = opt[0] == 'l'
+ ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
+ break;
+ }
+
+ if (opt[0] && opt[0] != '-')
+ break;
+
if (!strcmp(opt, "-l")) {
opt_request = REQ_VIEW_LOG;
continue;
if (!strcmp(opt, "-l")) {
opt_request = REQ_VIEW_LOG;
continue;
break;
}
break;
}
- if (!strcmp(opt, "log") ||
- !strcmp(opt, "diff") ||
- !strcmp(opt, "show")) {
- opt_request = opt[0] == 'l'
- ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
- break;
- }
-
- if (opt[0] && opt[0] != '-')
- break;
-
die("unknown option '%s'\n\n%s", opt, usage);
}
die("unknown option '%s'\n\n%s", opt, usage);
}
if (opt_request == REQ_VIEW_MAIN)
/* XXX: This is vulnerable to the user overriding
* options required for the main view parser. */
if (opt_request == REQ_VIEW_MAIN)
/* XXX: This is vulnerable to the user overriding
* options required for the main view parser. */
- string_copy(opt_cmd, "git log --stat --pretty=raw");
+ string_copy(opt_cmd, "git log --pretty=raw");
else
string_copy(opt_cmd, "git");
buf_size = strlen(opt_cmd);
else
string_copy(opt_cmd, "git");
buf_size = strlen(opt_cmd);
die("command too long");
opt_cmd[buf_size] = 0;
die("command too long");
opt_cmd[buf_size] = 0;
-
}
if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
}
if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
+LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
+LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL)
LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL)
{ 'g', REQ_TOGGLE_REV_GRAPH },
{ ':', REQ_PROMPT },
{ 'g', REQ_TOGGLE_REV_GRAPH },
{ ':', REQ_PROMPT },
- /* wgetch() with nodelay() enabled returns ERR when there's no input. */
- { ERR, REQ_NONE },
-
/* Using the ncurses SIGWINCH handler. */
{ KEY_RESIZE, REQ_SCREEN_RESIZE },
};
/* Using the ncurses SIGWINCH handler. */
{ KEY_RESIZE, REQ_SCREEN_RESIZE },
};
static struct view *display[2];
static unsigned int current_view;
static struct view *display[2];
static unsigned int current_view;
+/* Reading from the prompt? */
+static bool input_mode = FALSE;
+
#define foreach_displayed_view(view, i) \
for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
#define foreach_displayed_view(view, i) \
for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
{
struct line *line;
bool selected = (view->offset + lineno == view->lineno);
{
struct line *line;
bool selected = (view->offset + lineno == view->lineno);
+ bool draw_ok;
assert(view_is_displayed(view));
assert(view_is_displayed(view));
wclrtoeol(view->win);
}
wclrtoeol(view->win);
}
- return view->ops->draw(view, line, lineno, selected);
+ scrollok(view->win, FALSE);
+ draw_ok = view->ops->draw(view, line, lineno, selected);
+ scrollok(view->win, TRUE);
+
+ return draw_ok;
}
static void
}
static void
}
redrawwin(view->win);
}
redrawwin(view->win);
- wrefresh(view->win);
+ if (input_mode)
+ wnoutrefresh(view->win);
+ else
+ wrefresh(view->win);
}
static void
}
static void
static void
update_view_title(struct view *view)
{
static void
update_view_title(struct view *view)
{
- assert(view_is_displayed(view));
-
- if (view == display[current_view])
- wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
- else
- wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
-
- werase(view->title);
- wmove(view->title, 0, 0);
+ char buf[SIZEOF_STR];
+ char state[SIZEOF_STR];
+ size_t bufpos = 0, statelen = 0;
- if (*view->ref)
- wprintw(view->title, "[%s] %s", view->name, view->ref);
- else
- wprintw(view->title, "[%s]", view->name);
+ assert(view_is_displayed(view));
if (view->lines || view->pipe) {
unsigned int view_lines = view->offset + view->height;
if (view->lines || view->pipe) {
unsigned int view_lines = view->offset + view->height;
? MIN(view_lines, view->lines) * 100 / view->lines
: 0;
? MIN(view_lines, view->lines) * 100 / view->lines
: 0;
- wprintw(view->title, " - %s %d of %d (%d%%)",
- view->ops->type,
- view->lineno + 1,
- view->lines,
- lines);
+ string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
+ view->ops->type,
+ view->lineno + 1,
+ view->lines,
+ lines);
+
+ if (view->pipe) {
+ time_t secs = time(NULL) - view->start_time;
+
+ /* Three git seconds are a long time ... */
+ if (secs > 2)
+ string_format_from(state, &statelen, " %lds", secs);
+ }
}
}
- if (view->pipe) {
- time_t secs = time(NULL) - view->start_time;
+ string_format_from(buf, &bufpos, "[%s]", view->name);
+ if (*view->ref && bufpos < view->width) {
+ size_t refsize = strlen(view->ref);
+ size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
- /* Three git seconds are a long time ... */
- if (secs > 2)
- wprintw(view->title, " %lds", secs);
+ if (minsize < view->width)
+ refsize = view->width - minsize + 7;
+ string_format_from(buf, &bufpos, " %.*s", refsize, view->ref);
}
}
+ if (statelen && bufpos < view->width) {
+ string_format_from(buf, &bufpos, " %s", state);
+ }
+
+ if (view == display[current_view])
+ wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
+ else
+ wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
+
+ mvwaddnstr(view->title, 0, 0, buf, bufpos);
+ wclrtoeol(view->title);
wmove(view->title, 0, view->width - 1);
wmove(view->title, 0, view->width - 1);
- wrefresh(view->title);
+
+ if (input_mode)
+ wnoutrefresh(view->title);
+ else
+ wrefresh(view->title);
}
static void
}
static void
}
static void
}
static void
-update_display_cursor(void)
+update_display_cursor(struct view *view)
{
{
- struct view *view = display[current_view];
-
/* Move the cursor to the right-most column of the cursor line.
*
* XXX: This could turn out to be a bit expensive, but it ensures that
/* Move the cursor to the right-most column of the cursor line.
*
* XXX: This could turn out to be a bit expensive, but it ensures that
/* Scrolling backend */
static void
/* Scrolling backend */
static void
-do_scroll_view(struct view *view, int lines, bool redraw)
+do_scroll_view(struct view *view, int lines)
{
{
- assert(view_is_displayed(view));
+ bool redraw_current_line = FALSE;
/* The rendering expects the new offset. */
view->offset += lines;
/* The rendering expects the new offset. */
view->offset += lines;
assert(0 <= view->offset && view->offset < view->lines);
assert(lines);
assert(0 <= view->offset && view->offset < view->lines);
assert(lines);
+ /* Move current line into the view. */
+ if (view->lineno < view->offset) {
+ view->lineno = view->offset;
+ redraw_current_line = TRUE;
+ } else if (view->lineno >= view->offset + view->height) {
+ view->lineno = view->offset + view->height - 1;
+ redraw_current_line = TRUE;
+ }
+
+ assert(view->offset <= view->lineno && view->lineno < view->lines);
+
/* Redraw the whole screen if scrolling is pointless. */
if (view->height < ABS(lines)) {
redraw_view(view);
/* Redraw the whole screen if scrolling is pointless. */
if (view->height < ABS(lines)) {
redraw_view(view);
if (!draw_view_line(view, line))
break;
}
if (!draw_view_line(view, line))
break;
}
- }
-
- /* Move current line into the view. */
- if (view->lineno < view->offset) {
- view->lineno = view->offset;
- draw_view_line(view, 0);
- } else if (view->lineno >= view->offset + view->height) {
- view->lineno = view->offset + view->height - 1;
- draw_view_line(view, view->lineno - view->offset);
+ if (redraw_current_line)
+ draw_view_line(view, view->lineno - view->offset);
}
}
- assert(view->offset <= view->lineno && view->lineno < view->lines);
-
- if (!redraw)
- return;
-
redrawwin(view->win);
wrefresh(view->win);
report("");
redrawwin(view->win);
wrefresh(view->win);
report("");
{
int lines = 1;
{
int lines = 1;
+ assert(view_is_displayed(view));
+
switch (request) {
case REQ_SCROLL_PAGE_DOWN:
lines = view->height;
switch (request) {
case REQ_SCROLL_PAGE_DOWN:
lines = view->height;
die("request %d not handled in switch", request);
}
die("request %d not handled in switch", request);
}
- do_scroll_view(view, lines, TRUE);
+ do_scroll_view(view, lines);
}
/* Cursor moving */
static void
}
/* Cursor moving */
static void
-move_view(struct view *view, enum request request, bool redraw)
+move_view(struct view *view, enum request request)
{
{
+ int scroll_steps = 0;
int steps;
switch (request) {
int steps;
switch (request) {
view->lineno += steps;
assert(0 <= view->lineno && view->lineno < view->lines);
view->lineno += steps;
assert(0 <= view->lineno && view->lineno < view->lines);
- /* Repaint the old "current" line if we be scrolling */
- if (ABS(steps) < view->height)
- draw_view_line(view, view->lineno - steps - view->offset);
-
/* Check whether the view needs to be scrolled */
if (view->lineno < view->offset ||
view->lineno >= view->offset + view->height) {
/* Check whether the view needs to be scrolled */
if (view->lineno < view->offset ||
view->lineno >= view->offset + view->height) {
+ scroll_steps = steps;
if (steps < 0 && -steps > view->offset) {
if (steps < 0 && -steps > view->offset) {
- steps = -view->offset;
+ scroll_steps = -view->offset;
} else if (steps > 0) {
if (view->lineno == view->lines - 1 &&
view->lines > view->height) {
} else if (steps > 0) {
if (view->lineno == view->lines - 1 &&
view->lines > view->height) {
- steps = view->lines - view->offset - 1;
- if (steps >= view->height)
- steps -= view->height - 1;
+ scroll_steps = view->lines - view->offset - 1;
+ if (scroll_steps >= view->height)
+ scroll_steps -= view->height - 1;
}
}
}
}
+ }
- do_scroll_view(view, steps, redraw);
+ if (!view_is_displayed(view)) {
+ view->offset += steps;
+ view->ops->select(view, &view->line[view->lineno]);
return;
}
return;
}
- /* Draw the current line */
- draw_view_line(view, view->lineno - view->offset);
+ /* Repaint the old "current" line if we be scrolling */
+ if (ABS(steps) < view->height)
+ draw_view_line(view, view->lineno - steps - view->offset);
- if (!redraw)
+ if (scroll_steps) {
+ do_scroll_view(view, scroll_steps);
return;
return;
+ }
+
+ /* Draw the current line */
+ draw_view_line(view, view->lineno - view->offset);
redrawwin(view->win);
wrefresh(view->win);
redrawwin(view->win);
wrefresh(view->win);
* Searching
*/
* Searching
*/
-static void search_view(struct view *view, enum request request, const char *search);
+static void search_view(struct view *view, enum request request);
static bool
find_next_line(struct view *view, unsigned long lineno, struct line *line)
static bool
find_next_line(struct view *view, unsigned long lineno, struct line *line)
if (!*opt_search)
report("No previous search");
else
if (!*opt_search)
report("No previous search");
else
- search_view(view, request, opt_search);
+ search_view(view, request);
return;
}
return;
}
}
static void
}
static void
-search_view(struct view *view, enum request request, const char *search)
+search_view(struct view *view, enum request request)
{
int regex_err;
{
int regex_err;
return;
}
return;
}
- regex_err = regcomp(view->regex, search, REG_EXTENDED);
+ regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
if (regex_err != 0) {
char buf[SIZEOF_STR] = "unknown error";
if (regex_err != 0) {
char buf[SIZEOF_STR] = "unknown error";
return;
}
return;
}
- string_copy(view->grep, search);
+ string_copy(view->grep, opt_search);
find_next(view, request);
}
find_next(view, request);
}
report("Allocation failure");
end:
report("Allocation failure");
end:
+ view->ops->read(view, NULL);
end_update(view);
return FALSE;
}
end_update(view);
return FALSE;
}
/* Scroll the view that was split if the current line is
* outside the new limited view. */
/* Scroll the view that was split if the current line is
* outside the new limited view. */
- do_scroll_view(prev, lines, TRUE);
+ do_scroll_view(prev, lines);
}
if (prev && view != prev) {
}
if (prev && view != prev) {
case REQ_MOVE_PAGE_DOWN:
case REQ_MOVE_FIRST_LINE:
case REQ_MOVE_LAST_LINE:
case REQ_MOVE_PAGE_DOWN:
case REQ_MOVE_FIRST_LINE:
case REQ_MOVE_LAST_LINE:
- move_view(view, request, TRUE);
+ move_view(view, request);
break;
case REQ_SCROLL_LINE_DOWN:
break;
case REQ_SCROLL_LINE_DOWN:
view->parent == VIEW(REQ_VIEW_MAIN)) ||
(view == VIEW(REQ_VIEW_BLOB) &&
view->parent == VIEW(REQ_VIEW_TREE))) {
view->parent == VIEW(REQ_VIEW_MAIN)) ||
(view == VIEW(REQ_VIEW_BLOB) &&
view->parent == VIEW(REQ_VIEW_TREE))) {
- bool redraw = display[1] == view;
-
view = view->parent;
view = view->parent;
- move_view(view, request, redraw);
- if (redraw)
+ move_view(view, request);
+ if (view_is_displayed(view))
update_view_title(view);
} else {
update_view_title(view);
} else {
- move_view(view, request, TRUE);
+ move_view(view, request);
break;
}
/* Fall-through */
break;
}
/* Fall-through */
case REQ_SEARCH:
case REQ_SEARCH_BACK:
case REQ_SEARCH:
case REQ_SEARCH_BACK:
- search_view(view, request, opt_search);
+ search_view(view, request);
break;
case REQ_FIND_NEXT:
break;
case REQ_FIND_NEXT:
char *ref = NULL;
FILE *pipe;
char *ref = NULL;
FILE *pipe;
- if (!string_format(refbuf, "git describe %s", commit_id))
+ if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
return TRUE;
pipe = popen(refbuf, "r");
return TRUE;
pipe = popen(refbuf, "r");
do {
struct ref *ref = refs[refpos];
do {
struct ref *ref = refs[refpos];
- char *fmt = ref->tag ? "%s[%s]" : "%s%s";
+ char *fmt = ref->tag ? "%s[%s]" :
+ ref->remote ? "%s<%s>" : "%s%s";
if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
return;
if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
return;
{
struct line *line = &view->line[view->lines];
{
struct line *line = &view->line[view->lines];
+ if (!data)
+ return TRUE;
+
line->data = strdup(data);
if (!line->data)
return FALSE;
line->data = strdup(data);
if (!line->data)
return FALSE;
* Tree backend
*/
* Tree backend
*/
-/* Parse output from git ls-tree:
+/* Parse output from git-ls-tree(1):
*
* 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
* 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
*
* 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
* 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
static bool
tree_read(struct view *view, char *text)
{
static bool
tree_read(struct view *view, char *text)
{
- size_t textlen = strlen(text);
+ size_t textlen = text ? strlen(text) : 0;
char buf[SIZEOF_STR];
unsigned long pos;
enum line_type type;
char buf[SIZEOF_STR];
unsigned long pos;
enum line_type type;
static bool
tree_enter(struct view *view, struct line *line)
{
static bool
tree_enter(struct view *view, struct line *line)
{
- enum open_flags flags = OPEN_DEFAULT;
- char *data = line->data;
+ enum open_flags flags;
enum request request;
switch (line->type) {
enum request request;
switch (line->type) {
} else {
size_t pathlen = strlen(opt_path);
size_t origlen = pathlen;
} else {
size_t pathlen = strlen(opt_path);
size_t origlen = pathlen;
+ char *data = line->data;
char *basename = data + SIZEOF_TREE_ATTR;
if (!string_format_from(opt_path, &pathlen, "%s/", basename)) {
char *basename = data + SIZEOF_TREE_ATTR;
if (!string_format_from(opt_path, &pathlen, "%s/", basename)) {
/* Trees and subtrees share the same ID, so they are not not
* unique like blobs. */
/* Trees and subtrees share the same ID, so they are not not
* unique like blobs. */
- flags |= OPEN_RELOAD;
+ flags = OPEN_RELOAD;
request = REQ_VIEW_TREE;
break;
case LINE_TREE_FILE:
request = REQ_VIEW_TREE;
break;
case LINE_TREE_FILE:
- /* This causes the blob view to become split, and not having it
- * in the tree dir case will make the blob view automatically
- * disappear when moving to a different directory. */
- flags |= OPEN_SPLIT;
+ flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
request = REQ_VIEW_BLOB;
break;
request = REQ_VIEW_BLOB;
break;
open_view(view, request, flags);
open_view(view, request, flags);
- if (!VIEW(request)->pipe)
- return TRUE;
-
- /* For tree views insert the path to the parent as the first line. */
- if (request == REQ_VIEW_BLOB) {
- /* Mirror what is showed in the title bar. */
- string_ncopy(ref_blob, data + STRING_SIZE("100644 blob "), 40);
- string_copy(VIEW(REQ_VIEW_BLOB)->ref, ref_blob);
- return TRUE;
- }
-
return TRUE;
}
static void
tree_select(struct view *view, struct line *line)
{
return TRUE;
}
static void
tree_select(struct view *view, struct line *line)
{
- if (line->type == LINE_TREE_DIR || line->type == LINE_TREE_FILE) {
- char *text = line->data;
+ char *text = line->data;
- string_ncopy(view->ref, text + STRING_SIZE("100644 blob "), 40);
- string_copy(ref_blob, view->ref);
+ text += STRING_SIZE("100644 blob ");
+
+ if (line->type == LINE_TREE_FILE) {
+ string_ncopy(ref_blob, text, 40);
+ /* Also update the blob view's ref, since all there must always
+ * be in sync. */
+ string_copy(VIEW(REQ_VIEW_BLOB)->ref, ref_blob);
+
+ } else if (line->type != LINE_TREE_DIR) {
+ return;
}
}
+
+ string_ncopy(view->ref, text, 40);
}
static struct view_ops tree_ops = {
}
static struct view_ops tree_ops = {
/*
/*
- * Main view backend
+ * Revision graph
*/
struct commit {
*/
struct commit {
- char id[41]; /* SHA1 ID. */
- char title[75]; /* First line of the commit message. */
+ char id[SIZEOF_REV]; /* SHA1 ID. */
+ char title[128]; /* First line of the commit message. */
char author[75]; /* Author of the commit. */
struct tm time; /* Date from the author ident. */
struct ref **refs; /* Repository references. */
char author[75]; /* Author of the commit. */
struct tm time; /* Date from the author ident. */
struct ref **refs; /* Repository references. */
size_t graph_size; /* The width of the graph array. */
};
size_t graph_size; /* The width of the graph array. */
};
+/* Size of rev graph with no "padding" columns */
+#define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
+
+struct rev_graph {
+ struct rev_graph *prev, *next, *parents;
+ char rev[SIZEOF_REVITEMS][SIZEOF_REV];
+ size_t size;
+ struct commit *commit;
+ size_t pos;
+};
+
+/* Parents of the commit being visualized. */
+static struct rev_graph graph_parents[4];
+
+/* The current stack of revisions on the graph. */
+static struct rev_graph graph_stacks[4] = {
+ { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
+ { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
+ { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
+ { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
+};
+
+static inline bool
+graph_parent_is_merge(struct rev_graph *graph)
+{
+ return graph->parents->size > 1;
+}
+
+static inline void
+append_to_rev_graph(struct rev_graph *graph, chtype symbol)
+{
+ struct commit *commit = graph->commit;
+
+ if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
+ commit->graph[commit->graph_size++] = symbol;
+}
+
+static void
+done_rev_graph(struct rev_graph *graph)
+{
+ if (graph_parent_is_merge(graph) &&
+ graph->pos < graph->size - 1 &&
+ graph->next->size == graph->size + graph->parents->size - 1) {
+ size_t i = graph->pos + graph->parents->size - 1;
+
+ graph->commit->graph_size = i * 2;
+ while (i < graph->next->size - 1) {
+ append_to_rev_graph(graph, ' ');
+ append_to_rev_graph(graph, '\\');
+ i++;
+ }
+ }
+
+ graph->size = graph->pos = 0;
+ graph->commit = NULL;
+ memset(graph->parents, 0, sizeof(*graph->parents));
+}
+
+static void
+push_rev_graph(struct rev_graph *graph, char *parent)
+{
+ int i;
+
+ /* "Collapse" duplicate parents lines.
+ *
+ * FIXME: This needs to also update update the drawn graph but
+ * for now it just serves as a method for pruning graph lines. */
+ for (i = 0; i < graph->size; i++)
+ if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
+ return;
+
+ if (graph->size < SIZEOF_REVITEMS) {
+ string_ncopy(graph->rev[graph->size++], parent, SIZEOF_REV);
+ }
+}
+
+static chtype
+get_rev_graph_symbol(struct rev_graph *graph)
+{
+ chtype symbol;
+
+ if (graph->parents->size == 0)
+ symbol = REVGRAPH_INIT;
+ else if (graph_parent_is_merge(graph))
+ symbol = REVGRAPH_MERGE;
+ else if (graph->pos >= graph->size)
+ symbol = REVGRAPH_BRANCH;
+ else
+ symbol = REVGRAPH_COMMIT;
+
+ return symbol;
+}
+
+static void
+draw_rev_graph(struct rev_graph *graph)
+{
+ struct rev_filler {
+ chtype separator, line;
+ };
+ enum { DEFAULT, RSHARP, RDIAG, LDIAG };
+ static struct rev_filler fillers[] = {
+ { ' ', REVGRAPH_LINE },
+ { '`', '.' },
+ { '\'', ' ' },
+ { '/', ' ' },
+ };
+ chtype symbol = get_rev_graph_symbol(graph);
+ struct rev_filler *filler;
+ size_t i;
+
+ filler = &fillers[DEFAULT];
+
+ for (i = 0; i < graph->pos; i++) {
+ append_to_rev_graph(graph, filler->line);
+ if (graph_parent_is_merge(graph->prev) &&
+ graph->prev->pos == i)
+ filler = &fillers[RSHARP];
+
+ append_to_rev_graph(graph, filler->separator);
+ }
+
+ /* Place the symbol for this revision. */
+ append_to_rev_graph(graph, symbol);
+
+ if (graph->prev->size > graph->size)
+ filler = &fillers[RDIAG];
+ else
+ filler = &fillers[DEFAULT];
+
+ i++;
+
+ for (; i < graph->size; i++) {
+ append_to_rev_graph(graph, filler->separator);
+ append_to_rev_graph(graph, filler->line);
+ if (graph_parent_is_merge(graph->prev) &&
+ i < graph->prev->pos + graph->parents->size)
+ filler = &fillers[RSHARP];
+ if (graph->prev->size > graph->size)
+ filler = &fillers[LDIAG];
+ }
+
+ if (graph->prev->size > graph->size) {
+ append_to_rev_graph(graph, filler->separator);
+ if (filler->line != ' ')
+ append_to_rev_graph(graph, filler->line);
+ }
+}
+
+/* Prepare the next rev graph */
+static void
+prepare_rev_graph(struct rev_graph *graph)
+{
+ size_t i;
+
+ /* First, traverse all lines of revisions up to the active one. */
+ for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
+ if (!strcmp(graph->rev[graph->pos], graph->commit->id))
+ break;
+
+ push_rev_graph(graph->next, graph->rev[graph->pos]);
+ }
+
+ /* Interleave the new revision parent(s). */
+ for (i = 0; i < graph->parents->size; i++)
+ push_rev_graph(graph->next, graph->parents->rev[i]);
+
+ /* Lastly, put any remaining revisions. */
+ for (i = graph->pos + 1; i < graph->size; i++)
+ push_rev_graph(graph->next, graph->rev[i]);
+}
+
+static void
+update_rev_graph(struct rev_graph *graph)
+{
+ /* If this is the finalizing update ... */
+ if (graph->commit)
+ prepare_rev_graph(graph);
+
+ /* Graph visualization needs a one rev look-ahead,
+ * so the first update doesn't visualize anything. */
+ if (!graph->prev->commit)
+ return;
+
+ draw_rev_graph(graph->prev);
+ done_rev_graph(graph->prev->prev);
+}
+
+
+/*
+ * Main view backend
+ */
+
static bool
main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
{
static bool
main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
{
@@ -2734,6 +2966,7 @@ main_draw(struct view *view, struct line *line, unsigned int lineno, bool select
for (i = 0; i < commit->graph_size; i++)
waddch(view->win, commit->graph[i]);
for (i = 0; i < commit->graph_size; i++)
waddch(view->win, commit->graph[i]);
+ waddch(view->win, ' ');
col += commit->graph_size + 1;
}
col += commit->graph_size + 1;
}
@@ -2747,6 +2980,8 @@ main_draw(struct view *view, struct line *line, unsigned int lineno, bool select
;
else if (commit->refs[i]->tag)
wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
;
else if (commit->refs[i]->tag)
wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
+ else if (commit->refs[i]->remote)
+ wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
else
wattrset(view->win, get_line_attr(LINE_MAIN_REF));
waddstr(view->win, "[");
else
wattrset(view->win, get_line_attr(LINE_MAIN_REF));
waddstr(view->win, "[");
@@ -2778,10 +3013,18 @@ main_draw(struct view *view, struct line *line, unsigned int lineno, bool select
static bool
main_read(struct view *view, char *line)
{
static bool
main_read(struct view *view, char *line)
{
- enum line_type type = get_line_type(line);
+ static struct rev_graph *graph = graph_stacks;
+ enum line_type type;
struct commit *commit = view->lines
? view->line[view->lines - 1].data : NULL;
struct commit *commit = view->lines
? view->line[view->lines - 1].data : NULL;
+ if (!line) {
+ update_rev_graph(graph);
+ return TRUE;
+ }
+
+ type = get_line_type(line);
+
switch (type) {
case LINE_COMMIT:
commit = calloc(1, sizeof(struct commit));
switch (type) {
case LINE_COMMIT:
commit = calloc(1, sizeof(struct commit));
view->line[view->lines++].data = commit;
string_copy(commit->id, line);
commit->refs = get_refs(commit->id);
view->line[view->lines++].data = commit;
string_copy(commit->id, line);
commit->refs = get_refs(commit->id);
- commit->graph[commit->graph_size++] = ACS_LTEE;
+ graph->commit = commit;
+ break;
+
+ case LINE_PARENT:
+ if (commit) {
+ line += STRING_SIZE("parent ");
+ push_rev_graph(graph->parents, line);
+ }
break;
case LINE_AUTHOR:
{
break;
case LINE_AUTHOR:
{
+ /* Parse author lines where the name may be empty:
+ * author <email@address.tld> 1138474660 +0100
+ */
char *ident = line + STRING_SIZE("author ");
char *ident = line + STRING_SIZE("author ");
- char *end = strchr(ident, '<');
+ char *nameend = strchr(ident, '<');
+ char *emailend = strchr(ident, '>');
- if (!commit)
+ if (!commit || !nameend || !emailend)
break;
break;
- if (end) {
- char *email = end + 1;
-
- for (; end > ident && isspace(end[-1]); end--) ;
+ update_rev_graph(graph);
+ graph = graph->next;
- if (end == ident && *email) {
- ident = email;
- end = strchr(ident, '>');
- for (; end > ident && isspace(end[-1]); end--) ;
- }
- *end = 0;
+ *nameend = *emailend = 0;
+ ident = chomp_string(ident);
+ if (!*ident) {
+ ident = chomp_string(nameend + 1);
+ if (!*ident)
+ ident = "Unknown";
}
}
- /* End is NULL or ident meaning there's no author. */
- if (end <= ident)
- ident = "Unknown";
-
string_copy(commit->author, ident);
/* Parse epoch and timezone */
string_copy(commit->author, ident);
/* Parse epoch and timezone */
- if (end) {
- char *secs = strchr(end + 1, '>');
- char *zone;
- time_t time;
-
- if (!secs || secs[1] != ' ')
- break;
+ if (emailend[1] == ' ') {
+ char *secs = emailend + 2;
+ char *zone = strchr(secs, ' ');
+ time_t time = (time_t) atol(secs);
- secs += 2;
- time = (time_t) atol(secs);
- zone = strchr(secs, ' ');
if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
long tz;
if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
long tz;
time -= tz;
}
time -= tz;
}
+
gmtime_r(&time, &commit->time);
}
break;
gmtime_r(&time, &commit->time);
}
break;
/* Require titles to start with a non-space character at the
* offset used by git log. */
/* Require titles to start with a non-space character at the
* offset used by git log. */
- /* FIXME: More gracefull handling of titles; append "..." to
- * shortened titles, etc. */
- if (strncmp(line, " ", 4) ||
- isspace(line[4]))
+ if (strncmp(line, " ", 4))
+ break;
+ line += 4;
+ /* Well, if the title starts with a whitespace character,
+ * try to be forgiving. Otherwise we end up with no title. */
+ while (isspace(*line))
+ line++;
+ if (*line == '\0')
break;
break;
+ /* FIXME: More graceful handling of titles; append "..." to
+ * shortened titles, etc. */
- string_copy(commit->title, line + 4);
+ string_copy(commit->title, line);
}
return TRUE;
}
return TRUE;
/* The status window is used for polling keystrokes. */
static WINDOW *status_win;
/* The status window is used for polling keystrokes. */
static WINDOW *status_win;
+static bool status_empty = TRUE;
+
/* Update status and title window. */
static void
report(const char *msg, ...)
{
/* Update status and title window. */
static void
report(const char *msg, ...)
{
- static bool empty = TRUE;
struct view *view = display[current_view];
struct view *view = display[current_view];
- if (!empty || *msg) {
+ if (input_mode)
+ return;
+
+ if (!status_empty || *msg) {
va_list args;
va_start(args, msg);
va_list args;
va_start(args, msg);
- werase(status_win);
wmove(status_win, 0, 0);
if (*msg) {
vwprintw(status_win, msg, args);
wmove(status_win, 0, 0);
if (*msg) {
vwprintw(status_win, msg, args);
- empty = FALSE;
+ status_empty = FALSE;
} else {
} else {
- empty = TRUE;
+ status_empty = TRUE;
}
}
+ wclrtoeol(status_win);
wrefresh(status_win);
va_end(args);
}
update_view_title(view);
wrefresh(status_win);
va_end(args);
}
update_view_title(view);
- update_display_cursor();
+ update_display_cursor(view);
}
/* Controls when nodelay should be in effect when polling user input. */
}
/* Controls when nodelay should be in effect when polling user input. */
struct view *view;
int i, key;
struct view *view;
int i, key;
+ input_mode = TRUE;
+
foreach_view (view, i)
update_view(view);
foreach_view (view, i)
update_view(view);
- report("%s%.*s", prompt, pos, buf);
+ input_mode = FALSE;
+
+ mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
+ wclrtoeol(status_win);
+
/* Refresh, accept single keystroke of input */
key = wgetch(status_win);
switch (key) {
/* Refresh, accept single keystroke of input */
key = wgetch(status_win);
switch (key) {
}
}
}
}
- if (status == CANCEL) {
- /* Clear the status window */
- report("");
+ /* Clear the status window */
+ status_empty = FALSE;
+ report("");
+
+ if (status == CANCEL)
return NULL;
return NULL;
- }
buf[pos++] = 0;
buf[pos++] = 0;
{
struct ref *ref;
bool tag = FALSE;
{
struct ref *ref;
bool tag = FALSE;
+ bool remote = FALSE;
if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
/* Commits referenced by tags has "^{}" appended. */
if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
/* Commits referenced by tags has "^{}" appended. */
namelen -= STRING_SIZE("refs/tags/");
name += STRING_SIZE("refs/tags/");
namelen -= STRING_SIZE("refs/tags/");
name += STRING_SIZE("refs/tags/");
+ } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
+ remote = TRUE;
+ namelen -= STRING_SIZE("refs/remotes/");
+ name += STRING_SIZE("refs/remotes/");
+
} else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
namelen -= STRING_SIZE("refs/heads/");
name += STRING_SIZE("refs/heads/");
} else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
namelen -= STRING_SIZE("refs/heads/");
name += STRING_SIZE("refs/heads/");
strncpy(ref->name, name, namelen);
ref->name[namelen] = 0;
ref->tag = tag;
strncpy(ref->name, name, namelen);
ref->name[namelen] = 0;
ref->tag = tag;
+ ref->remote = remote;
string_copy(ref->id, id);
return OK;
string_copy(ref->id, id);
return OK;
/* Refresh, accept single keystroke of input */
key = wgetch(status_win);
/* Refresh, accept single keystroke of input */
key = wgetch(status_win);
+ /* wgetch() with nodelay() enabled returns ERR when there's no
+ * input. */
+ if (key == ERR) {
+ request = REQ_NONE;
+ continue;
+ }
+
request = get_keybinding(display[current_view]->keymap, key);
/* Some low-level request handling. This keeps access to
request = get_keybinding(display[current_view]->keymap, key);
/* Some low-level request handling. This keeps access to