commit 28cf319f780087bba863715f31d4ec417eb87a6d from: Stefan Sperling date: Thu Jan 28 17:06:22 2021 UTC add 'got commit -F' option to commit with a log message stored in a file To avoid accidents commit -F opens the prepared log message in an editor so it can be reviewed before the commit is created. For non-interactive use the -N option is required in addition to -F. ok millert@ commit - a5a46be28fbbc316a4ce0adb5d92571a59a7e115 commit + 28cf319f780087bba863715f31d4ec417eb87a6d blob - 5a69c331382003ee5c2d946293c4d1647cf56088 blob + 1991d4245ec041b192b623d07a0b5659e4757156 --- got/got.1 +++ got/got.1 @@ -1216,7 +1216,7 @@ is a directory. .It Cm rv Short alias for .Cm revert . -.It Cm commit Oo Fl m Ar message Oc Oo Fl S Oc Op Ar path ... +.It Cm commit Oo Fl F Ar path Oc Oo Fl m Ar message Oc Oo Fl N Oc Oo Fl S Oc Op Ar path ... Create a new commit in the repository from changes in a work tree and use this commit as the new base commit for the work tree. If no @@ -1229,6 +1229,17 @@ If changes have been explicitly staged for commit with only commit staged changes and reject any specified paths which have not been staged. .Pp +.Cm got commit +opens a temporary file in an editor where a log message can be written +unless the +.Fl m +option is used +or the +.Fl F +and +.Fl N +options are used together. +.Pp Show the status of each affected file, using the following status codes: .Bl -column YXZ description .It M Ta modified file @@ -1269,13 +1280,28 @@ The options for .Cm got commit are as follows: .Bl -tag -width Ds -.It Fl m Ar message -Use the specified log message when creating the new commit. -Without the +.It Fl F Ar path +Use the prepared log message stored in the file found at +.Ar path +when creating the new commit. +.Cm got commit +opens a temporary file in an editor where the prepared log message can be +reviewed and edited further if needed. +Cannot be used together with the .Fl m -option, +option. +.It Fl m Ar message +Use the specified log message when creating the new commit. +Cannot be used together with the +.Fl F +option. +.It Fl N +This option prevents .Cm got commit -opens a temporary file in an editor where a log message can be written. +from opening the commit message in an editor. +It has no effect unless it is used together with the +.Fl F +option and is intended for non-interactive use such as scripting. .It Fl S Allow the addition of symbolic links which point outside of the path space that is under version control. @@ -2126,16 +2152,9 @@ with a pre-defined log message. .Pp Alternatively, create a new commit from local changes in a work tree directory with a log message that has been prepared in the file -.Pa /tmp/msg . -If -.Xr vi 1 -is set as the -.Ev EDITOR , -.Pa /tmp/msg -can be read into the buffer for review: +.Pa /tmp/msg: .Pp -.Dl $ got commit -.Dl :r /tmp/msg +.Dl $ got commit -F /tmp/msg .Pp Update any work tree checked out from the .Dq unified-buffer-cache blob - f1d690299d64d615136d348d8a119f0828400162 blob + 7a85d64a195dba5931737769c04f02796efa3258 --- got/got.c +++ got/got.c @@ -431,7 +431,8 @@ doneediting: static const struct got_error * edit_logmsg(char **logmsg, const char *editor, const char *logmsg_path, - const char *initial_content, size_t initial_content_len, int check_comments) + const char *initial_content, size_t initial_content_len, + int require_modification) { const struct got_error *err = NULL; char *line = NULL; @@ -453,7 +454,8 @@ edit_logmsg(char **logmsg, const char *editor, const c if (stat(logmsg_path, &st2) == -1) return got_error_from_errno("stat"); - if (st.st_mtime == st2.st_mtime && st.st_size == st2.st_size) + if (require_modification && + st.st_mtime == st2.st_mtime && st.st_size == st2.st_size) return got_error_msg(GOT_ERR_COMMIT_MSG_EMPTY, "no changes made to commit message, aborting"); @@ -526,7 +528,8 @@ edit_logmsg(char **logmsg, const char *editor, const c "commit message cannot be empty, aborting"); goto done; } - if (check_comments && strcmp(*logmsg, initial_content_stripped) == 0) + if (require_modification && + strcmp(*logmsg, initial_content_stripped) == 0) err = got_error_msg(GOT_ERR_COMMIT_MSG_EMPTY, "no changes made to commit message, aborting"); done: @@ -6927,13 +6930,15 @@ done: __dead static void usage_commit(void) { - fprintf(stderr, "usage: %s commit [-m msg] [-S] [path ...]\n", - getprogname()); + fprintf(stderr, "usage: %s commit [-F path] [-m msg] [-N] [-S] " + "[path ...]\n", getprogname()); exit(1); } struct collect_commit_logmsg_arg { const char *cmdline_log; + const char *prepared_log; + int non_interactive; const char *editor; const char *worktree_path; const char *branch_name; @@ -6941,6 +6946,56 @@ struct collect_commit_logmsg_arg { char *logmsg_path; }; + +static const struct got_error * +read_prepared_logmsg(char **logmsg, const char *path) +{ + const struct got_error *err = NULL; + FILE *f = NULL; + struct stat sb; + size_t r; + + *logmsg = NULL; + memset(&sb, 0, sizeof(sb)); + + f = fopen(path, "r"); + if (f == NULL) + return got_error_from_errno2("fopen", path); + + if (fstat(fileno(f), &sb) == -1) { + err = got_error_from_errno2("fstat", path); + goto done; + } + if (sb.st_size == 0) { + err = got_error(GOT_ERR_COMMIT_MSG_EMPTY); + goto done; + } + + *logmsg = malloc(sb.st_size + 1); + if (*logmsg == NULL) { + err = got_error_from_errno("malloc"); + goto done; + } + + r = fread(*logmsg, 1, sb.st_size, f); + if (r != sb.st_size) { + if (ferror(f)) + err = got_error_from_errno2("fread", path); + else + err = got_error(GOT_ERR_IO); + goto done; + } + (*logmsg)[sb.st_size] = '\0'; +done: + if (fclose(f) == EOF && err == NULL) + err = got_error_from_errno2("fclose", path); + if (err) { + free(*logmsg); + *logmsg = NULL; + } + return err; + +} static const struct got_error * collect_commit_logmsg(struct got_pathlist_head *commitable_paths, char **logmsg, @@ -6963,20 +7018,36 @@ collect_commit_logmsg(struct got_pathlist_head *commit return got_error_from_errno("malloc"); strlcpy(*logmsg, a->cmdline_log, len); return NULL; - } + } else if (a->prepared_log != NULL && a->non_interactive) + return read_prepared_logmsg(logmsg, a->prepared_log); if (asprintf(&template, "%s/logmsg", a->worktree_path) == -1) return got_error_from_errno("asprintf"); + err = got_opentemp_named_fd(&a->logmsg_path, &fd, template); + if (err) + goto done; + + if (a->prepared_log) { + char *msg; + err = read_prepared_logmsg(&msg, a->prepared_log); + if (err) + goto done; + if (write(fd, msg, strlen(msg)) == -1) { + err = got_error_from_errno2("write", a->logmsg_path); + free(msg); + goto done; + } + free(msg); + } + initial_content_len = asprintf(&initial_content, "\n# changes to be committed on branch %s:\n", a->branch_name); - if (initial_content_len == -1) - return got_error_from_errno("asprintf"); - - err = got_opentemp_named_fd(&a->logmsg_path, &fd, template); - if (err) + if (initial_content_len == -1) { + err = got_error_from_errno("asprintf"); goto done; + } if (write(fd, initial_content, initial_content_len) == -1) { err = got_error_from_errno2("write", a->logmsg_path); @@ -6991,7 +7062,7 @@ collect_commit_logmsg(struct got_pathlist_head *commit } err = edit_logmsg(logmsg, a->editor, a->logmsg_path, initial_content, - initial_content_len, 1); + initial_content_len, a->prepared_log ? 0 : 1); done: free(initial_content); free(template); @@ -7018,19 +7089,33 @@ cmd_commit(int argc, char *argv[]) char *cwd = NULL, *id_str = NULL; struct got_object_id *id = NULL; const char *logmsg = NULL; + char *prepared_logmsg = NULL; struct collect_commit_logmsg_arg cl_arg; char *gitconfig_path = NULL, *editor = NULL, *author = NULL; int ch, rebase_in_progress, histedit_in_progress, preserve_logmsg = 0; - int allow_bad_symlinks = 0; + int allow_bad_symlinks = 0, non_interactive = 0; struct got_pathlist_head paths; TAILQ_INIT(&paths); cl_arg.logmsg_path = NULL; - while ((ch = getopt(argc, argv, "m:S")) != -1) { + while ((ch = getopt(argc, argv, "F:m:NS")) != -1) { switch (ch) { + case 'F': + if (logmsg != NULL) + option_conflict('F', 'm'); + prepared_logmsg = realpath(optarg, NULL); + if (prepared_logmsg == NULL) + return got_error_from_errno2("realpath", + optarg); + break; case 'm': + if (prepared_logmsg) + option_conflict('m', 'F'); logmsg = optarg; + break; + case 'N': + non_interactive = 1; break; case 'S': allow_bad_symlinks = 1; @@ -7104,6 +7189,8 @@ cmd_commit(int argc, char *argv[]) cl_arg.editor = editor; cl_arg.cmdline_log = logmsg; + cl_arg.prepared_log = prepared_logmsg; + cl_arg.non_interactive = non_interactive; cl_arg.worktree_path = got_worktree_get_root_path(worktree); cl_arg.branch_name = got_worktree_get_head_ref_name(worktree); if (!histedit_in_progress) { @@ -7145,6 +7232,7 @@ done: free(gitconfig_path); free(editor); free(author); + free(prepared_logmsg); return error; } blob - 1e055a9b1f81d1721ec308592989b045d4f027c9 blob + 3956dc0289b56e9ce806eab6702d0522b243de79 --- regress/cmdline/commit.sh +++ regress/cmdline/commit.sh @@ -1356,7 +1356,107 @@ test_commit_fix_bad_symlink() { test_done "$testroot" "0" } + +test_commit_prepared_logmsg() { + local testroot=`test_init commit_prepared_logmsg` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + echo "modified alpha" > $testroot/wt/alpha + (cd $testroot/wt && got rm beta >/dev/null) + echo "new file" > $testroot/wt/new + (cd $testroot/wt && got add new >/dev/null) + + echo 'test commit_prepared_logmsg' > $testroot/logmsg + + cat > $testroot/editor.sh < $testroot/stdout) + local head_rev=`git_show_head $testroot/repo` + echo "A new" > $testroot/stdout.expected + echo "M alpha" >> $testroot/stdout.expected + echo "D beta" >> $testroot/stdout.expected + echo "Created commit $head_rev" >> $testroot/stdout.expected + + cmp -s $testroot/stdout.expected $testroot/stdout + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + local author_time=`git_show_author_time $testroot/repo` + d=`env TZ=UTC date -r $author_time +"%a %b %e %X %Y UTC"` + echo "-----------------------------------------------" > $testroot/stdout.expected + echo "commit $head_rev (master)" >> $testroot/stdout.expected + echo "from: $GOT_AUTHOR" >> $testroot/stdout.expected + echo "date: $d" >> $testroot/stdout.expected + echo " " >> $testroot/stdout.expected + echo " test commit_prepared_logmsg" >> $testroot/stdout.expected + echo " " >> $testroot/stdout.expected + + (cd $testroot/wt && got log -l 1 > $testroot/stdout) + cmp -s $testroot/stdout.expected $testroot/stdout + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + echo "modified alpha again" > $testroot/wt/alpha + + echo 'test commit_prepared_logmsg non-interactive' \ + > $testroot/logmsg + + (cd $testroot/wt && got commit -N -F "$testroot/logmsg" \ + > $testroot/stdout) + + local head_rev=`git_show_head $testroot/repo` + echo "M alpha" > $testroot/stdout.expected + echo "Created commit $head_rev" >> $testroot/stdout.expected + + cmp -s $testroot/stdout.expected $testroot/stdout + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + local author_time=`git_show_author_time $testroot/repo` + d=`env TZ=UTC date -r $author_time +"%a %b %e %X %Y UTC"` + echo "-----------------------------------------------" \ + > $testroot/stdout.expected + echo "commit $head_rev (master)" >> $testroot/stdout.expected + echo "from: $GOT_AUTHOR" >> $testroot/stdout.expected + echo "date: $d" >> $testroot/stdout.expected + echo " " >> $testroot/stdout.expected + echo " test commit_prepared_logmsg non-interactive" \ + >> $testroot/stdout.expected + echo " " >> $testroot/stdout.expected + + (cd $testroot/wt && got log -l 1 > $testroot/stdout) + cmp -s $testroot/stdout.expected $testroot/stdout + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + fi + test_done "$testroot" "$ret" +} + test_parseargs "$@" run_test test_commit_basic run_test test_commit_new_subdir @@ -1382,3 +1482,4 @@ run_test test_commit_normalizes_filemodes run_test test_commit_with_unrelated_submodule run_test test_commit_symlink run_test test_commit_fix_bad_symlink +run_test test_commit_prepared_logmsg