-/* Copyright (c) 2006 Jonas Fonseca <fonseca@diku.dk>
+/* Copyright (c) 2006-2007 Jonas Fonseca <fonseca@diku.dk>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
#define COLOR_DEFAULT (-1)
#define ICONV_NONE ((iconv_t) -1)
-#ifndef ICONV_INBUF_TYPE
-#define ICONV_INBUF_TYPE char *
+#ifndef ICONV_CONST
+#define ICONV_CONST /* nothing */
#endif
/* The format and size of the date column in the main view. */
REQ_(NEXT, "Move to next"), \
REQ_(PREVIOUS, "Move to previous"), \
REQ_(VIEW_NEXT, "Move focus to next view"), \
+ REQ_(REFRESH, "Reload and refresh"), \
REQ_(VIEW_CLOSE, "Close the current view"), \
REQ_(QUIT, "Close all views and quit"), \
\
REQ_(FIND_PREV, "Find previous search match"), \
\
REQ_GROUP("Misc") \
- REQ_(NONE, "Do nothing"), \
REQ_(PROMPT, "Bring up the prompt"), \
REQ_(SCREEN_REDRAW, "Redraw the screen"), \
REQ_(SCREEN_RESIZE, "Resize the screen"), \
REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
REQ_(STATUS_UPDATE, "Update file status"), \
- REQ_(EDIT, "Open in editor")
+ REQ_(STATUS_MERGE, "Merge file using external tool"), \
+ REQ_(EDIT, "Open in editor"), \
+ REQ_(NONE, "Do nothing")
/* User action requests. */
/* Offset all requests to avoid conflicts with ncurses getch values. */
REQ_OFFSET = KEY_MAX + 1,
- REQ_INFO,
- REQ_UNKNOWN,
+ REQ_INFO
#undef REQ_GROUP
#undef REQ_
!string_enum_compare(req_info[i].name, name, namelen))
return req_info[i].request;
- return REQ_UNKNOWN;
+ return REQ_NONE;
}
static char opt_search[SIZEOF_STR] = "";
static char opt_cdup[SIZEOF_STR] = "";
static char opt_git_dir[SIZEOF_STR] = "";
+static char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
static char opt_editor[SIZEOF_STR] = "";
enum option_type {
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(STAT_SECTION, "", COLOR_DEFAULT, COLOR_BLUE, A_BOLD), \
+LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
-LINE(STAT_STAGED, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
-LINE(STAT_UNSTAGED,"", COLOR_YELLOW, COLOR_DEFAULT, 0), \
+LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
+LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0)
enum line_type {
{ KEY_RETURN, REQ_ENTER },
{ KEY_UP, REQ_PREVIOUS },
{ KEY_DOWN, REQ_NEXT },
+ { 'R', REQ_REFRESH },
/* Cursor navigation */
{ 'k', REQ_MOVE_UP },
{ 'g', REQ_TOGGLE_REV_GRAPH },
{ ':', REQ_PROMPT },
{ 'u', REQ_STATUS_UPDATE },
+ { 'M', REQ_STATUS_MERGE },
{ 'e', REQ_EDIT },
/* Using the ncurses SIGWINCH handler. */
}
static char *
+get_key_name(int key_value)
+{
+ static char key_char[] = "'X'";
+ char *seq = NULL;
+ int key;
+
+ for (key = 0; key < ARRAY_SIZE(key_table); key++)
+ if (key_table[key].value == key_value)
+ seq = key_table[key].name;
+
+ if (seq == NULL &&
+ key_value < 127 &&
+ isprint(key_value)) {
+ key_char[1] = (char) key_value;
+ seq = key_char;
+ }
+
+ return seq ? seq : "'?'";
+}
+
+static char *
get_key(enum request request)
{
static char buf[BUFSIZ];
return buf;
}
+struct run_request {
+ enum keymap keymap;
+ int key;
+ char cmd[SIZEOF_STR];
+};
+
+static struct run_request *run_request;
+static size_t run_requests;
+
+static enum request
+add_run_request(enum keymap keymap, int key, int argc, char **argv)
+{
+ struct run_request *tmp;
+ struct run_request req = { keymap, key };
+ size_t bufpos;
+
+ for (bufpos = 0; argc > 0; argc--, argv++)
+ if (!string_format_from(req.cmd, &bufpos, "%s ", *argv))
+ return REQ_NONE;
+
+ req.cmd[bufpos - 1] = 0;
+
+ tmp = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
+ if (!tmp)
+ return REQ_NONE;
+
+ run_request = tmp;
+ run_request[run_requests++] = req;
+
+ return REQ_NONE + run_requests;
+}
+
+static struct run_request *
+get_run_request(enum request request)
+{
+ if (request <= REQ_NONE)
+ return NULL;
+ return &run_request[request - REQ_NONE - 1];
+}
+
+static void
+add_builtin_run_requests(void)
+{
+ struct {
+ enum keymap keymap;
+ int key;
+ char *argv[1];
+ } reqs[] = {
+ { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
+ { KEYMAP_GENERIC, 'G', { "git gc" } },
+ };
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(reqs); i++) {
+ enum request req;
+
+ req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
+ if (req != REQ_NONE)
+ add_keybinding(reqs[i].keymap, req, reqs[i].key);
+ }
+}
/*
* User config file handling.
int keymap;
int key;
- if (argc != 3) {
+ if (argc < 3) {
config_msg = "Wrong number of arguments given to bind command";
return ERR;
}
}
request = get_request(argv[2]);
- if (request == REQ_UNKNOWN) {
+ if (request = REQ_NONE) {
+ const char *obsolete[] = { "cherry-pick" };
+ size_t namelen = strlen(argv[2]);
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
+ if (namelen == strlen(obsolete[i]) &&
+ !string_enum_compare(obsolete[i], argv[2], namelen)) {
+ config_msg = "Obsolete request name";
+ return ERR;
+ }
+ }
+ }
+ if (request == REQ_NONE && *argv[2]++ == '!')
+ request = add_run_request(keymap, key, argc - 2, argv + 2);
+ if (request == REQ_NONE) {
config_msg = "Unknown request name";
return ERR;
}
config_lineno = 0;
config_errors = FALSE;
+ add_builtin_run_requests();
+
if (!home || !string_format(buf, "%s/.tigrc", home))
return ERR;
line[linelen - 1] = 0;
if (opt_iconv != ICONV_NONE) {
- ICONV_INBUF_TYPE inbuf = line;
+ ICONV_CONST char *inbuf = line;
size_t inlen = linelen;
char *outbuf = out_buffer;
}
static void
-open_editor(struct view *view, char *file)
+open_external_viewer(const char *cmd)
+{
+ def_prog_mode(); /* save current tty modes */
+ endwin(); /* restore original tty modes */
+ system(cmd);
+ fprintf(stderr, "Press Enter to continue");
+ getc(stdin);
+ reset_prog_mode();
+ redraw_display();
+}
+
+static void
+open_mergetool(const char *file)
+{
+ char cmd[SIZEOF_STR];
+ char file_sq[SIZEOF_STR];
+
+ if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
+ string_format(cmd, "git mergetool %s", file_sq)) {
+ open_external_viewer(cmd);
+ }
+}
+
+static void
+open_editor(bool from_root, const char *file)
{
char cmd[SIZEOF_STR];
char file_sq[SIZEOF_STR];
char *editor;
+ char *prefix = from_root ? opt_cdup : "";
editor = getenv("GIT_EDITOR");
if (!editor && *opt_editor)
editor = "vi";
if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
- string_format(cmd, "%s %s", editor, file_sq)) {
- def_prog_mode(); /* save current tty modes */
- endwin(); /* restore original tty modes */
- system(cmd);
- reset_prog_mode();
- redraw_display();
+ string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
+ open_external_viewer(cmd);
}
}
+static void
+open_run_request(enum request request)
+{
+ struct run_request *req = get_run_request(request);
+ char buf[SIZEOF_STR * 2];
+ size_t bufpos;
+ char *cmd;
+
+ if (!req) {
+ report("Unknown run request");
+ return;
+ }
+
+ bufpos = 0;
+ cmd = req->cmd;
+
+ while (cmd) {
+ char *next = strstr(cmd, "%(");
+ int len = next - cmd;
+ char *value;
+
+ if (!next) {
+ len = strlen(cmd);
+ value = "";
+
+ } else if (!strncmp(next, "%(head)", 7)) {
+ value = ref_head;
+
+ } else if (!strncmp(next, "%(commit)", 9)) {
+ value = ref_commit;
+
+ } else if (!strncmp(next, "%(blob)", 7)) {
+ value = ref_blob;
+
+ } else {
+ report("Unknown replacement in run request: `%s`", req->cmd);
+ return;
+ }
+
+ if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
+ return;
+
+ if (next)
+ next = strchr(next, ')') + 1;
+ cmd = next;
+ }
+
+ open_external_viewer(buf);
+}
+
/*
* User request switch noodle
*/
{
int i;
+ if (request == REQ_NONE) {
+ doupdate();
+ return TRUE;
+ }
+
+ if (request > REQ_NONE) {
+ open_run_request(request);
+ return TRUE;
+ }
+
if (view && view->lines) {
request = view->ops->request(view, request, &view->line[view->lineno]);
if (request == REQ_NONE)
open_view(view, request, OPEN_DEFAULT);
break;
+ case REQ_VIEW_STATUS:
+ if (opt_is_inside_work_tree == FALSE) {
+ report("The status view requires a working tree");
+ break;
+ }
+ open_view(view, request, OPEN_DEFAULT);
+ break;
+
case REQ_VIEW_MAIN:
case REQ_VIEW_DIFF:
case REQ_VIEW_LOG:
case REQ_VIEW_TREE:
case REQ_VIEW_HELP:
- case REQ_VIEW_STATUS:
open_view(view, request, OPEN_DEFAULT);
break;
report("");
break;
}
+ case REQ_REFRESH:
+ report("Refreshing is not yet supported for the %s view", view->name);
+ break;
+
case REQ_TOGGLE_LINENO:
opt_line_number = !opt_line_number;
redraw_display();
report("Nothing to edit");
break;
+
case REQ_ENTER:
report("Nothing to enter");
break;
- case REQ_NONE:
- doupdate();
- return TRUE;
case REQ_VIEW_CLOSE:
/* XXX: Mark closed views by letting view->parent point to the
if (!req_info[i].request)
lines++;
+ lines += run_requests + 1;
+
view->line = calloc(lines, sizeof(*view->line));
if (!view->line)
return FALSE;
for (i = 0; i < ARRAY_SIZE(req_info); i++) {
char *key;
+ if (req_info[i].request == REQ_NONE)
+ continue;
+
if (!req_info[i].request) {
add_line_text(view, "", LINE_DEFAULT);
add_line_text(view, req_info[i].help, LINE_DEFAULT);
}
key = get_key(req_info[i].request);
+ if (!*key)
+ key = "(no key defined)";
+
if (!string_format(buf, " %-25s %s", key, req_info[i].help))
continue;
add_line_text(view, buf, LINE_DEFAULT);
}
+ if (run_requests) {
+ add_line_text(view, "", LINE_DEFAULT);
+ add_line_text(view, "External commands:", LINE_DEFAULT);
+ }
+
+ for (i = 0; i < run_requests; i++) {
+ struct run_request *req = get_run_request(REQ_NONE + i + 1);
+ char *key;
+
+ if (!req)
+ continue;
+
+ key = get_key_name(req->key);
+ if (!*key)
+ key = "(no key defined)";
+
+ if (!string_format(buf, " %-10s %-14s `%s`",
+ keymap_table[req->keymap].name,
+ key, req->cmd))
+ continue;
+
+ add_line_text(view, buf, LINE_DEFAULT);
+ }
+
return TRUE;
}
status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
{
struct status *file = NULL;
+ struct status *unmerged = NULL;
char buf[SIZEOF_STR * 4];
size_t bufsize = 0;
FILE *pipe;
if (!sep)
break;
sepsize = sep - buf + 1;
+
+ /* Collapse all 'M'odified entries that
+ * follow a associated 'U'nmerged entry.
+ */
+ if (file->status == 'U') {
+ unmerged = file;
+
+ } else if (unmerged) {
+ int collapse = !strcmp(buf, unmerged->name);
+
+ unmerged = NULL;
+ if (collapse) {
+ free(file);
+ view->lines--;
+ continue;
+ }
+ }
}
/* git-ls-files just delivers a NUL separated
return TRUE;
}
-#define STATUS_DIFF_INDEX_CMD "git diff-index -z --cached HEAD"
+/* Don't show unmerged entries in the staged section. */
+#define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached HEAD"
#define STATUS_DIFF_FILES_CMD "git diff-files -z"
#define STATUS_LIST_OTHER_CMD \
"git ls-files -z --others --exclude-per-directory=.gitignore"
struct stat statbuf;
char exclude[SIZEOF_STR];
char cmd[SIZEOF_STR];
+ unsigned long prev_lineno = view->lineno;
size_t i;
for (i = 0; i < view->lines; i++)
free(view->line[i].data);
free(view->line);
- view->lines = view->line_size = 0;
+ view->lines = view->line_size = view->lineno = 0;
view->line = NULL;
if (!realloc_lines(view, view->line_size + 6))
!status_run(view, cmd, FALSE, LINE_STAT_UNTRACKED))
return FALSE;
+ /* If all went well restore the previous line number to stay in
+ * the context. */
+ if (prev_lineno < view->lines)
+ view->lineno = prev_lineno;
+ else
+ view->lineno = view->lines - 1;
+
return TRUE;
}
break;
default:
- die("w00t");
+ die("line type %d not handled in switch", line->type);
}
open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
break;
default:
- die("w00t");
+ die("line type %d not handled in switch", type);
}
pipe = popen(cmd, "w");
} else if (!status_update_file(view, line->data, line->type)) {
report("Failed to update file status");
}
-
- open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
}
static enum request
status_update(view);
break;
+ case REQ_STATUS_MERGE:
+ open_mergetool(status->name);
+ break;
+
case REQ_EDIT:
if (!status)
return request;
- open_editor(view, status->name);
+ open_editor(status->status != '?', status->name);
break;
case REQ_ENTER:
+ /* After returning the status view has been split to
+ * show the stage view. No further reloading is
+ * necessary. */
status_enter(view, line);
+ return REQ_NONE;
+
+ case REQ_REFRESH:
+ /* Simply reload the view. */
break;
default:
return request;
}
+ open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
+
return REQ_NONE;
}
struct status *status = line->data;
char file[SIZEOF_STR] = "all files";
char *text;
+ char *key;
if (status && !string_format(file, "'%s'", status->name))
return;
break;
default:
- die("w00t");
+ die("line type %d not handled in switch", line->type);
+ }
+
+ if (status && status->status == 'U') {
+ text = "Press %s to resolve conflict in %s";
+ key = get_key(REQ_STATUS_MERGE);
+
+ } else {
+ key = get_key(REQ_STATUS_UPDATE);
}
- string_format(view->ref, text, get_key(REQ_STATUS_UPDATE), file);
+ string_format(view->ref, text, key, file);
}
static bool
if (!stage_status.name[0])
return request;
- open_editor(view, stage_status.name);
+ open_editor(stage_status.status != '?', stage_status.name);
break;
case REQ_ENTER:
if (input_mode)
return;
+ if (!view) {
+ char buf[SIZEOF_STR];
+ va_list args;
+
+ va_start(args, msg);
+ if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
+ buf[sizeof(buf) - 1] = 0;
+ buf[sizeof(buf) - 2] = '.';
+ buf[sizeof(buf) - 3] = '.';
+ buf[sizeof(buf) - 4] = '.';
+ }
+ va_end(args);
+ die("%s", buf);
+ }
+
if (!status_empty || *msg) {
va_list args;
static int
read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
{
- if (!opt_git_dir[0])
+ if (!opt_git_dir[0]) {
string_ncopy(opt_git_dir, name, namelen);
- else
+
+ } else if (opt_is_inside_work_tree == -1) {
+ /* This can be 3 different values depending on the
+ * version of git being used. If git-rev-parse does not
+ * understand --is-inside-work-tree it will simply echo
+ * the option else either "true" or "false" is printed.
+ * Default to true for the unknown case. */
+ opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
+
+ } else {
string_ncopy(opt_cdup, name, namelen);
+ }
+
return OK;
}
static int
load_repo_info(void)
{
- return read_properties(popen("git rev-parse --git-dir --show-cdup 2>/dev/null", "r"),
+ return read_properties(popen("git rev-parse --git-dir --is-inside-work-tree --show-cdup 2>/dev/null", "r"),
"=", read_repo_info);
}
if (load_repo_info() == ERR)
die("Failed to load repo info.");
- /* Require a git repository unless when running in pager mode. */
- if (!opt_git_dir[0])
- die("Not a git repository");
-
if (load_options() == ERR)
die("Failed to load user config.");
if (!parse_options(argc, argv))
return 0;
+ /* Require a git repository unless when running in pager mode. */
+ if (!opt_git_dir[0])
+ die("Not a git repository");
+
if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
opt_iconv = iconv_open(opt_codeset, opt_encoding);
if (opt_iconv == ICONV_NONE)