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/git.cxx | 4 +- bdep/git.hxx | 31 ++++++++- bdep/git.ixx | 19 ++++++ bdep/git.txx | 18 +++++- bdep/release.cli | 29 +++++++-- bdep/release.cxx | 163 +++++++++++++++++++++++++++++++++++++++++++---- tests/release.testscript | 126 +++++++++++++++++++++++++++++++++++- 7 files changed, 365 insertions(+), 25 deletions(-) diff --git a/bdep/git.cxx b/bdep/git.cxx index 91964aa..9509275 100644 --- a/bdep/git.cxx +++ b/bdep/git.cxx @@ -45,7 +45,7 @@ namespace bdep } optional - git_line (process&& pr, fdpipe&& pipe, bool ie) + git_line (process&& pr, fdpipe&& pipe, bool ie, char delim) { optional r; @@ -56,7 +56,7 @@ namespace bdep ifdstream is (move (pipe.in), fdstream_mode::skip, ifdstream::badbit); string l; - if (!eof (getline (is, l))) + if (!eof (getline (is, l, delim))) r = move (l); is.close (); // Detect errors. diff --git a/bdep/git.hxx b/bdep/git.hxx index e699947..c674926 100644 --- a/bdep/git.hxx +++ b/bdep/git.hxx @@ -31,6 +31,19 @@ namespace bdep I&& in, O&& out, E&& err, A&&... args); + template + inline process + start_git (const semantic_version& min_ver, + dir_path& repo, + I&& in, O&& out, E&& err, + A&&... args) + { + return start_git (min_ver, + const_cast (repo), + forward (in), forward (out), forward (err), + forward (args)...); + } + // Wait for git process to terminate. // void @@ -70,7 +83,23 @@ namespace bdep // redirected output pipe. // optional - git_line (process&& pr, fdpipe&& pipe, bool ignore_error); + git_line (process&&, fdpipe&&, bool ignore_error, char delim = '\n'); + + // Similar to git_line() functions but return the complete git output. + // + template + optional + git_string (const semantic_version&, bool ignore_error, A&&... args); + + template + optional + git_string (const semantic_version&, + const dir_path& repo, + bool ignore_error, + A&&... args); + + optional + git_string (process&&, fdpipe&&, bool ignore_error); // Try to derive a remote HTTPS repository URL from the optionally specified // custom git config value falling back to remote.origin.build2Url and then diff --git a/bdep/git.ixx b/bdep/git.ixx index dbc43cb..f06a000 100644 --- a/bdep/git.ixx +++ b/bdep/git.ixx @@ -35,4 +35,23 @@ namespace bdep "-C", repo, forward (args)...); } + + template + inline optional + git_string (const semantic_version& min_ver, + const dir_path& repo, + bool ie, + A&&... args) + { + return git_string (min_ver, + ie, + "-C", repo, + forward (args)...); + } + + inline optional + git_string (process&& pr, fdpipe&& pipe, bool ignore_error) + { + return git_line (move (pr), move (pipe), ignore_error, '\0' /* delim */); + } } diff --git a/bdep/git.txx b/bdep/git.txx index 5ff5e6c..f3bedf0 100644 --- a/bdep/git.txx +++ b/bdep/git.txx @@ -89,7 +89,7 @@ namespace bdep template optional - git_line (const semantic_version& min_ver, bool ie, A&&... args) + git_line (const semantic_version& min_ver, bool ie, char delim, A&&... args) { fdpipe pipe (open_pipe ()); auto_fd null (ie ? open_dev_null () : auto_fd ()); @@ -100,7 +100,21 @@ namespace bdep ie ? null.get () : 2 /* stderr */, forward (args)...)); - return git_line (move (pr), move (pipe), ie); + return git_line (move (pr), move (pipe), ie, delim); + } + + template + inline optional + git_line (const semantic_version& min_ver, bool ie, A&&... args) + { + return git_line (min_ver, ie, '\n' /* delim */, forward (args)...); + } + + template + inline optional + git_string (const semantic_version& min_ver, bool ie, A&&... args) + { + return git_line (min_ver, ie, '\0' /* delim */, forward (args)...); } template diff --git a/bdep/release.cli b/bdep/release.cli index 2dadb7f..768cba1 100644 --- a/bdep/release.cli +++ b/bdep/release.cli @@ -44,11 +44,16 @@ namespace bdep package's \cb{manifest} file, commits these changes (unless \cb{--no-commit} is specified), tags this commit (unless \cb{--no-tag} is specified), and, if \cb{--push} is specified, pushes the changes to the - remote. Note that in this case the project's repository index is expected - to already contain other changes since for a revision all the associated - changes, including to version, must belong to a single commit. In this - mode \cb{release} will also silently replace an existing tag for the same - version. + remote. Note that in this mode \cb{release} will also silently replace an + existing tag for the same version. + + When releasing a revision, the project's repository index is expected to + already contain other changes since for a revision all the associated + changes, including to version, must belong to a single commit. + Alternatively, a revision can be released by amending one or more + existing commits using the \cb{--amend} and \cb{--squash} options. In + this case the index may still contain additional changes but is not + required to. The \cb{release} command also has a number of \i{continue modes} that allow the completion of steps that were previously suppressed with the @@ -123,6 +128,20 @@ namespace bdep "Open the next development cycle instead of releasing a new version." } + bool --amend + { + "Release a revision by amending the latest commit instead of making + a new one." + } + + size_t --squash = 1 + { + "", + "Release a revision by squashing the specified number of previous + commits and then amending the result. Requires the \cb{--amend} + option to be specified." + } + bool --alpha { "Release an alpha instead of the final version." 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) diff --git a/tests/release.testscript b/tests/release.testscript index 2f63265..1ba4cbc 100644 --- a/tests/release.testscript +++ b/tests/release.testscript @@ -651,6 +651,124 @@ log2 = $gp2 log '--pretty=format:"%d %s"' Create EOO } + + : amend + : + { + test.arguments += --push --amend --no-edit + + +$clone_repos + + +echo '' >+ prj/repositories.manifest + +$gp commit -a -m 'Fix repositories.manifest' + + +echo '' >+ prj/buildfile + +$gp commit -a -m 'Fix buildfile' -m "Add '\n' to the end of the file." + + +echo '' >+ prj/manifest + +$gp add manifest + + : no-squash + : + { + $clone_repos; + + $* 2>>~%EOE%; + %Updated tag 'v0.1.0' \(was \.*\)%d + EOE + + $clone2; + $log2 >>:~%EOO%; + % \(HEAD -> master, tag: v0.1.0, \.*\) Release version 0.1.0\+1%d + Fix repositories.manifest + Release version 0.1.0 + Create + EOO + + $log2 '--pretty=format:%B' >>EOO + Release version 0.1.0+1 + + Fix buildfile + + Add 'n' to the end of the file. + + Fix repositories.manifest + + Release version 0.1.0 + + Create + EOO + } + + : squash + : + { + $clone_repos; + + $* --squash 2 2>>~%EOE%; + %Updated tag 'v0.1.0' \(was \.*\)%d + EOE + + $clone2; + $log2 >>:~%EOO%; + % \(HEAD -> master, tag: v0.1.0, \.*\) Release version 0.1.0\+1%d + Release version 0.1.0 + Create + EOO + + $log2 '--pretty=format:%B' >>EOO + Release version 0.1.0+1 + + Fix buildfile + + Add 'n' to the end of the file. + + Fix repositories.manifest + + Release version 0.1.0 + + Create + EOO + } + + : no-changes-staged + : + { + $clone_repos; + + $gp commit -a -m 'Change manifest'; + + $* 2>>~%EOE%; + %Updated tag 'v0.1.0' \(was \.*\)%d + EOE + + $clone2; + $log2 >>:~%EOO%; + % \(HEAD -> master, tag: v0.1.0, \.*\) Release version 0.1.0\+1%d + Fix buildfile + Fix repositories.manifest + Release version 0.1.0 + Create + EOO + + + $log2 '--pretty=format:%B' >>EOO + Release version 0.1.0+1 + + Change manifest + + Fix buildfile + + Add 'n' to the end of the file. + + Fix repositories.manifest + + Release version 0.1.0 + + Create + EOO + } + } } : open @@ -1016,5 +1134,11 @@ log2 = $gp2 log '--pretty=format:"%d %s"' $* --push --show-push 2>'error: both --push and --show-push specified' != 0; $* --edit --no-commit 2>'error: both --no-commit and --edit specified' != 0; - $* --open-base 1.2.3 --open-beta 2>'error: both --open-beta and --open-base specified' != 0 + $* --open-base 1.2.3 --open-beta 2>'error: both --open-beta and --open-base specified' != 0; + + $* --amend 2>'error: --amend requires --revision' != 0; + $* --squash 1 2>'error: --squash requires --amend' != 0; + $* --revision --amend --squash 0 2>'error: invalid --squash value: 0' != 0; + $* --revision --amend --no-commit 2>'error: both --amend and --no-commit specified' != 0 + } -- cgit v1.1