#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
#include <unistd.h>
#include <time.h>
-#include <sys/types.h>
#include <regex.h>
#include <locale.h>
static void __NORETURN die(const char *err, ...);
static void report(const char *msg, ...);
-static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, int, char *, int));
+static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
static void set_nonblocking_input(bool loading);
static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
REQ_(SHOW_VERSION, "Show version information"), \
REQ_(STOP_LOADING, "Stop all loading views"), \
REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
- REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization")
+ REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
+ REQ_(STATUS_UPDATE, "Update file status"), \
+ REQ_(EDIT, "Open in editor")
/* User action requests. */
static iconv_t opt_iconv = ICONV_NONE;
static char opt_search[SIZEOF_STR] = "";
static char opt_cdup[SIZEOF_STR] = "";
+static char opt_git_dir[SIZEOF_STR] = "";
+static char opt_editor[SIZEOF_STR] = "";
enum option_type {
OPT_NONE,
if (!strcmp(opt, "-S")) {
opt_request = REQ_VIEW_STATUS;
- break;
+ continue;
}
if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
{ '.', REQ_TOGGLE_LINENO },
{ 'g', REQ_TOGGLE_REV_GRAPH },
{ ':', REQ_PROMPT },
+ { 'u', REQ_STATUS_UPDATE },
+ { 'e', REQ_EDIT },
/* Using the ncurses SIGWINCH handler. */
{ KEY_RESIZE, REQ_SCREEN_RESIZE },
}
static int
-read_option(char *opt, int optlen, char *value, int valuelen)
+read_option(char *opt, size_t optlen, char *value, size_t valuelen)
{
int status = OK;
} else {
/* Look for comment endings in the value. */
- int len = strcspn(value, "#");
+ size_t len = strcspn(value, "#");
if (len < valuelen) {
valuelen = len;
if (status == ERR) {
fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
- config_lineno, optlen, opt, config_msg);
+ config_lineno, (int) optlen, opt, config_msg);
config_errors = TRUE;
}
bool (*read)(struct view *view, char *data);
/* Draw one line; @lineno must be < view->height. */
bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
- /* Depending on view, change display based on current line. */
- bool (*enter)(struct view *view, struct line *line);
+ /* Depending on view handle a special requests. */
+ enum request (*request)(struct view *view, enum request request, struct line *line);
/* Search for regex in a line. */
bool (*grep)(struct view *view, struct line *line);
/* Select line */
VIEW_(LOG, "log", &pager_ops, ref_head),
VIEW_(TREE, "tree", &tree_ops, ref_commit),
VIEW_(BLOB, "blob", &blob_ops, ref_blob),
- VIEW_(HELP, "help", &help_ops, ""),
- VIEW_(PAGER, "pager", &pager_ops, ""),
+ VIEW_(HELP, "help", &help_ops, ""),
+ VIEW_(PAGER, "pager", &pager_ops, "stdin"),
VIEW_(STATUS, "status", &status_ops, ""),
};
assert(view_is_displayed(view));
- if (view->lines || view->pipe) {
+ if (!VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
unsigned int view_lines = view->offset + view->height;
unsigned int lines = view->lines
? MIN(view_lines, view->lines) * 100 / view->lines
if (minsize < view->width)
refsize = view->width - minsize + 7;
- string_format_from(buf, &bufpos, " %.*s", refsize, view->ref);
+ string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
}
if (statelen && bufpos < view->width) {
update_view_title(view);
}
+static void
+open_editor(struct view *view, char *file)
+{
+ char cmd[SIZEOF_STR];
+ char file_sq[SIZEOF_STR];
+ char *editor;
+
+ editor = getenv("GIT_EDITOR");
+ if (!editor && *opt_editor)
+ editor = opt_editor;
+ if (!editor)
+ editor = getenv("VISUAL");
+ if (!editor)
+ editor = getenv("EDITOR");
+ if (!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();
+ }
+}
/*
* User request switch noodle
{
int i;
+ if (view && view->lines) {
+ request = view->ops->request(view, request, &view->line[view->lineno]);
+ if (request == REQ_NONE)
+ return TRUE;
+ }
+
switch (request) {
case REQ_MOVE_UP:
case REQ_MOVE_DOWN:
break;
case REQ_VIEW_PAGER:
- if (!VIEW(REQ_VIEW_PAGER)->lines) {
+ if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
report("No pager content, press %s to run command from prompt",
get_key(REQ_PROMPT));
break;
view->parent == VIEW(REQ_VIEW_MAIN)) ||
(view == VIEW(REQ_VIEW_BLOB) &&
view->parent == VIEW(REQ_VIEW_TREE))) {
+ int line;
+
view = view->parent;
+ line = view->lineno;
move_view(view, request);
if (view_is_displayed(view))
update_view_title(view);
+ if (line == view->lineno)
+ break;
} else {
move_view(view, request);
break;
}
/* Fall-through */
- case REQ_ENTER:
- if (!view->lines) {
- report("Nothing to enter");
- break;
- }
- return view->ops->enter(view, &view->line[view->lineno]);
case REQ_VIEW_NEXT:
{
redraw_display();
break;
+ case REQ_EDIT:
+ report("Nothing to edit");
+ break;
+
+ case REQ_ENTER:
+ report("Nothing to enter");
+ break;
+
case REQ_NONE:
doupdate();
return TRUE;
return TRUE;
}
-static bool
-pager_enter(struct view *view, struct line *line)
+static enum request
+pager_request(struct view *view, enum request request, struct line *line)
{
int split = 0;
+ if (request != REQ_ENTER)
+ return request;
+
if (line->type == LINE_COMMIT &&
(view == VIEW(REQ_VIEW_LOG) ||
view == VIEW(REQ_VIEW_PAGER))) {
if (split)
update_view_title(view);
- return TRUE;
+ return REQ_NONE;
}
static bool
NULL,
pager_read,
pager_draw,
- pager_enter,
+ pager_request,
pager_grep,
pager_select,
};
help_open,
NULL,
pager_draw,
- pager_enter,
+ pager_request,
pager_grep,
pager_select,
};
return TRUE;
}
-static bool
-tree_enter(struct view *view, struct line *line)
+static enum request
+tree_request(struct view *view, enum request request, struct line *line)
{
enum open_flags flags;
- enum request request;
+
+ if (request != REQ_ENTER)
+ return request;
switch (line->type) {
case LINE_TREE_DIR:
open_view(view, request, flags);
- return TRUE;
+ return REQ_NONE;
}
static void
NULL,
tree_read,
pager_draw,
- tree_enter,
+ tree_request,
pager_grep,
tree_select,
};
NULL,
blob_read,
pager_draw,
- pager_enter,
+ pager_request,
pager_grep,
pager_select,
};
#define STATUS_DIFF_INDEX_CMD "git diff-index -z --cached HEAD"
#define STATUS_DIFF_FILES_CMD "git diff-files -z"
#define STATUS_LIST_OTHER_CMD \
- "_git_exclude=$(git rev-parse --git-dir)/info/exclude;" \
- "test -f \"$_git_exclude\" && exclude=\"--exclude-from=$_git_exclude\";" \
- "git ls-files -z --others --exclude-per-directory=.gitignore \"$exclude\"" \
+ "git ls-files -z --others --exclude-per-directory=.gitignore"
+
+#define STATUS_DIFF_SHOW_CMD \
+ "git diff --root --patch-with-stat --find-copies-harder -B -C %s -- %s 2>/dev/null"
/* First parse staged info using git-diff-index(1), then parse unstaged
* info using git-diff-files(1), and finally untracked files using
static bool
status_open(struct view *view)
{
+ struct stat statbuf;
+ char exclude[SIZEOF_STR];
+ char cmd[SIZEOF_STR];
size_t i;
for (i = 0; i < view->lines; i++)
if (!realloc_lines(view, view->line_size + 6))
return FALSE;
+ if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
+ return FALSE;
+
+ string_copy(cmd, STATUS_LIST_OTHER_CMD);
+
+ if (stat(exclude, &statbuf) >= 0) {
+ size_t cmdsize = strlen(cmd);
+
+ if (!string_format_from(cmd, &cmdsize, " %s", "--exclude-from=") ||
+ sq_quote(cmd, cmdsize, exclude) >= sizeof(cmd))
+ return FALSE;
+ }
+
if (!status_run(view, STATUS_DIFF_INDEX_CMD, TRUE, LINE_STAT_STAGED) ||
!status_run(view, STATUS_DIFF_FILES_CMD, TRUE, LINE_STAT_UNSTAGED) ||
- !status_run(view, STATUS_LIST_OTHER_CMD, FALSE, LINE_STAT_UNTRACKED))
+ !status_run(view, cmd, FALSE, LINE_STAT_UNTRACKED))
return FALSE;
return TRUE;
return TRUE;
}
-static bool
+static enum request
status_enter(struct view *view, struct line *line)
{
struct status *status = line->data;
+ char path[SIZEOF_STR] = "";
+ char *info;
+ size_t cmdsize = 0;
+
+ if (line->type == LINE_STAT_NONE ||
+ (!status && line[1].type == LINE_STAT_NONE)) {
+ report("No file to diff");
+ return REQ_NONE;
+ }
+
+ if (status && sq_quote(path, 0, status->name) >= sizeof(path))
+ return REQ_QUIT;
+
+ if (opt_cdup[0] &&
+ line->type != LINE_STAT_UNTRACKED &&
+ !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
+ return REQ_QUIT;
+
+ switch (line->type) {
+ case LINE_STAT_STAGED:
+ if (!string_format_from(opt_cmd, &cmdsize, STATUS_DIFF_SHOW_CMD,
+ "--cached", path))
+ return REQ_QUIT;
+ if (status)
+ info = "Staged changes to %s";
+ else
+ info = "Staged changes";
+ break;
+
+ case LINE_STAT_UNSTAGED:
+ if (!string_format_from(opt_cmd, &cmdsize, STATUS_DIFF_SHOW_CMD,
+ "", path))
+ return REQ_QUIT;
+ if (status)
+ info = "Unstaged changes to %s";
+ else
+ info = "Unstaged changes";
+ break;
+
+ case LINE_STAT_UNTRACKED:
+ if (opt_pipe)
+ return REQ_QUIT;
+
+
+ if (!status) {
+ report("No file to show");
+ return REQ_NONE;
+ }
+
+ opt_pipe = fopen(status->name, "r");
+ info = "Untracked file %s";
+ break;
+
+ default:
+ die("w00t");
+ }
+
+ open_view(view, REQ_VIEW_DIFF, OPEN_RELOAD | OPEN_SPLIT);
+ if (view_is_displayed(VIEW(REQ_VIEW_DIFF))) {
+ string_format(VIEW(REQ_VIEW_DIFF)->ref, info, status->name);
+ }
+
+ return REQ_NONE;
+}
+
+
+static bool
+status_update_file(struct view *view, struct status *status, enum line_type type)
+{
char cmd[SIZEOF_STR];
char buf[SIZEOF_STR];
size_t cmdsize = 0;
size_t written = 0;
FILE *pipe;
- if (!status)
- return TRUE;
-
if (opt_cdup[0] &&
- line->type != LINE_STAT_UNTRACKED &&
+ type != LINE_STAT_UNTRACKED &&
!string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
return FALSE;
- switch (line->type) {
+ switch (type) {
case LINE_STAT_STAGED:
if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
status->old.mode,
if (written != bufsize)
return FALSE;
- open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
return TRUE;
}
static void
+status_update(struct view *view)
+{
+ struct line *line = &view->line[view->lineno];
+
+ assert(view->lines);
+
+ if (!line->data) {
+ while (++line < view->line + view->lines && line->data) {
+ if (!status_update_file(view, line->data, line->type))
+ report("Failed to update file status");
+ }
+
+ if (!line[-1].data) {
+ report("Nothing to update");
+ return;
+ }
+
+ } 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_request(struct view *view, enum request request, struct line *line)
+{
+ struct status *status = line->data;
+
+ switch (request) {
+ case REQ_STATUS_UPDATE:
+ status_update(view);
+ break;
+
+ case REQ_EDIT:
+ if (!status)
+ return request;
+
+ open_editor(view, status->name);
+ break;
+
+ case REQ_ENTER:
+ status_enter(view, line);
+ break;
+
+ default:
+ return request;
+ }
+
+ return REQ_NONE;
+}
+
+static void
status_select(struct view *view, struct line *line)
{
+ struct status *status = line->data;
+ char file[SIZEOF_STR] = "all files";
char *text;
+ if (status && !string_format(file, "'%s'", status->name))
+ return;
+
+ if (!status && line[1].type == LINE_STAT_NONE)
+ line++;
+
switch (line->type) {
case LINE_STAT_STAGED:
- text = "Press Enter to unstage file for commit";
+ text = "Press %s to unstage %s for commit";
break;
case LINE_STAT_UNSTAGED:
- text = "Press Enter to stage file for commit ";
+ text = "Press %s to stage %s for commit";
break;
case LINE_STAT_UNTRACKED:
- text = "Press Enter to stage file for addition";
+ text = "Press %s to stage %s for addition";
break;
case LINE_STAT_NONE:
- return;
+ text = "Nothing to update";
+ break;
default:
die("w00t");
}
- string_ncopy(view->ref, text, strlen(text));
+ string_format(view->ref, text, get_key(REQ_STATUS_UPDATE), file);
}
static bool
status_open,
NULL,
status_draw,
- status_enter,
+ status_request,
status_grep,
status_select,
};
return TRUE;
}
-static bool
-main_enter(struct view *view, struct line *line)
+static enum request
+main_request(struct view *view, enum request request, struct line *line)
{
enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
- open_view(view, REQ_VIEW_DIFF, flags);
- return TRUE;
+ if (request == REQ_ENTER)
+ open_view(view, REQ_VIEW_DIFF, flags);
+ else
+ return request;
+
+ return REQ_NONE;
}
static bool
NULL,
main_read,
main_draw,
- main_enter,
+ main_request,
main_grep,
main_select,
};
}
static int
-read_ref(char *id, int idlen, char *name, int namelen)
+read_ref(char *id, size_t idlen, char *name, size_t namelen)
{
struct ref *ref;
bool tag = FALSE;
}
static int
-read_repo_config_option(char *name, int namelen, char *value, int valuelen)
+read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
{
if (!strcmp(name, "i18n.commitencoding"))
string_ncopy(opt_encoding, value, valuelen);
+ if (!strcmp(name, "core.editor"))
+ string_ncopy(opt_editor, value, valuelen);
+
return OK;
}
}
static int
-read_repo_info(char *name, int namelen, char *value, int valuelen)
+read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
{
- if (!opt_cdup[0])
+ if (!opt_git_dir[0])
+ string_ncopy(opt_git_dir, name, namelen);
+ else
string_ncopy(opt_cdup, name, namelen);
return OK;
}
+/* XXX: The line outputted by "--show-cdup" can be empty so the option
+ * must be the last one! */
static int
load_repo_info(void)
{
- return read_properties(popen("git rev-parse --show-cdup", "r"),
+ return read_properties(popen("git rev-parse --git-dir --show-cdup 2>/dev/null", "r"),
"=", read_repo_info);
}
static int
read_properties(FILE *pipe, const char *separators,
- int (*read_property)(char *, int, char *, int))
+ int (*read_property)(char *, size_t, char *, size_t))
{
char buffer[BUFSIZ];
char *name;
string_ncopy(opt_codeset, codeset, strlen(codeset));
}
+ 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 (load_repo_config() == ERR)
die("Failed to load repo config.");
- if (load_repo_info() == ERR)
- die("Failed to load repo info.");
-
if (!parse_options(argc, argv))
return 0;
if (load_refs() == ERR)
die("Failed to load refs.");
- /* Require a git repository unless when running in pager mode. */
- if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
- die("Not a git repository");
-
for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
view->cmd_env = getenv(view->cmd_env);