From bf02b66c61d941a60e45520ef77f677dad36557e Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Wed, 10 Apr 2019 22:56:22 +0300 Subject: Add --amend and --squash options to bdep-release --- bdep/release.cxx | 163 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 149 insertions(+), 14 deletions(-) (limited to 'bdep/release.cxx') diff --git a/bdep/release.cxx b/bdep/release.cxx index c6eef28..71c6372 100644 --- a/bdep/release.cxx +++ b/bdep/release.cxx @@ -393,10 +393,12 @@ namespace bdep // Note: the use-case for forcing would be to create a commit to be // squashed later. // - // Also, don't complain if we are not committing. + // Also, don't complain if we are not committing or amending an existing + // commit. // if (!st.staged && !o.no_commit () && + !o.amend () && o.force ().find ("unchanged") == o.force ().end ()) fail << "project directory has no staged changes" << info << "revision increment must be committed together with " @@ -458,6 +460,16 @@ namespace bdep } }; + // Verify that an option is specified only if it's prerequisite option + // is also specified. + // + auto require = [] (const char* opt, bool opt_specified, + const char* prereq, bool prereq_specified) + { + if (opt_specified && !prereq_specified) + fail << opt << " requires " << prereq; + }; + // Check the mode options. // verify ("--revision", o.revision ()); @@ -519,6 +531,20 @@ namespace bdep verify ("--push", o.push ()); verify ("--show-push", o.show_push ()); verify ("--no-commit", o.no_commit ()); // Not for tagging (see above). + + // Verify the --amend and --squash options. + // + require ("--amend", o.amend (), "--revision", o.revision ()); + require ("--squash", o.squash_specified (), "--amend", o.amend ()); + + if (o.squash_specified () && o.squash () == 0) + fail << "invalid --squash value: " << o.squash (); + + // There is no sense to amend without committing. + // + gopt = nullptr; + verify ("--amend", o.amend ()); // Revising only (see above). + verify ("--no-commit", o.no_commit ()); // Not for tagging (see above). } // Fully parse package manifest verifying it is valid and returning the @@ -774,22 +800,56 @@ namespace bdep return 1; } - // Stage and commit the project changes. Open the commit message in the - // editor if requested and --no-edit is not specified or if --edit is - // specified. + // Stage the project changes and either commit them separately or amend + // the latest commit. Open the commit messages in the editor if requested + // and --no-edit is not specified or if --edit is specified. Fail if the + // process terminated abnormally or with a non-zero status, unless + // return_error is true in which case return the git process exit + // information. // - auto commit_project = [&o, &prj] (const string& msg, bool edit) + auto commit_project = [&o, &prj] (const strings& msgs, + bool edit, + bool amend = false, + bool return_error = false) { + assert (!msgs.empty ()); + + cstrings msg_ops; + for (const string& m: msgs) + { + msg_ops.push_back ("-m"); + msg_ops.push_back (m.c_str ()); + } + // We shouldn't have any untracked files or unstaged changes other than // our modifications, so -a is good enough. // - run_git (git_ver, - prj.path, - "commit", - verb < 1 ? "-q" : verb >= 2 ? "-v" : nullptr, - "-a", - (edit && !o.no_edit ()) || o.edit () ? "-e" : nullptr, - "-m", msg); + // We don't want git to print anything to stdout, so let's redirect it + // to stderr for good measure, as git is known to print some + // informational messages to stdout. + // + process pr ( + start_git (git_ver, + prj.path, + 0 /* stdin */, + 2 /* stdout */, + 2 /* stderr */, + "commit", + verb < 1 ? "-q" : verb >= 2 ? "-v" : nullptr, + amend ? "--amend" : nullptr, + "-a", + (edit && !o.no_edit ()) || o.edit () ? "-e" : nullptr, + msg_ops)); + + // Wait for the process termination. + // + if (return_error) + pr.wait (); + else + finish_git (pr); // Fails on process error. + + assert (pr.exit); + return move (*pr.exit); }; // Release. @@ -863,7 +923,82 @@ namespace bdep else m = "Release version " + pkg.release_version->string (); - commit_project (m, st.staged); + strings msgs ({move (m)}); + + if (o.amend ()) + { + // Collect the amended/squashed commit messages. + // + // This would fail if we try to squash initial commit. However, that + // shouldn't be a problem since that would mean squashing the commit + // for the first release that we are now revising, which is + // meaningless. + // + optional ms ( + git_string (git_ver, + prj.path, + false /* ignore_error */, + "log", + "--format=%B", + "HEAD~" + to_string (o.squash ()) + "..HEAD")); + + if (!ms) + fail << "unable to obtain commit messages for " << o.squash () + << " latest commit(s)"; + + msgs.push_back (move (*ms)); + } + + // Note that we could handle the latest commit amendment (squash == 1) + // here as well, but let's do it via the git-commit --amend option to + // revert automatically in case of git terminating due to SIGINT, etc. + // + if (o.squash () > 1) + { + // Squash commits, reverting them into the index. + // + run_git (git_ver, + prj.path, + "reset", + verb < 1 ? "-q" : nullptr, + "--soft", + "HEAD~" + to_string (o.squash ())); + + // Commit editing the message. + // + process_exit e (commit_project (msgs, + true /* edit */, + false /* amend */, + true /* ignore_error */)); + + // If git-commit terminated normally with non-zero status, for example + // due to the empty commit message, then revert the above reset and + // fail afterwards. If it terminated abnormally, we probably shouldn't + // mess with it and leave things to the user to handle. + // + if (!e) + { + if (e.normal ()) + { + run_git (git_ver, + prj.path, + "reset", + verb < 1 ? "-q" : nullptr, + "--soft", + "HEAD@{1}"); // Previously checked out revision. + + throw failed (); // Assume the child issued diagnostics. + } + + fail << "git " << e; + } + } + else + { + // Commit or amend. + // + commit_project (msgs, st.staged || o.amend () /* edit */, o.amend ()); + } // The (possibly) staged changes are now committed. // @@ -932,7 +1067,7 @@ namespace bdep // Commit the manifest rewrites. // - commit_project ("Change version to " + ov, st.staged); + commit_project ({"Change version to " + ov}, st.staged); } if (push) -- cgit v1.1