aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2019-04-10 22:56:22 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2019-04-16 16:27:18 +0300
commitbf02b66c61d941a60e45520ef77f677dad36557e (patch)
tree0d61c6a06e868aeee2d6375a8581f87a47eac5b1
parent842b2c6604be98a7f5d05cb674ae121d716eeb64 (diff)
Add --amend and --squash options to bdep-release
-rw-r--r--bdep/git.cxx4
-rw-r--r--bdep/git.hxx31
-rw-r--r--bdep/git.ixx19
-rw-r--r--bdep/git.txx18
-rw-r--r--bdep/release.cli29
-rw-r--r--bdep/release.cxx163
-rw-r--r--tests/release.testscript126
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<string>
- git_line (process&& pr, fdpipe&& pipe, bool ie)
+ git_line (process&& pr, fdpipe&& pipe, bool ie, char delim)
{
optional<string> 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 <typename I, typename O, typename E, typename... A>
+ 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<const dir_path&> (repo),
+ forward<I> (in), forward<O> (out), forward<E> (err),
+ forward<A> (args)...);
+ }
+
// Wait for git process to terminate.
//
void
@@ -70,7 +83,23 @@ namespace bdep
// redirected output pipe.
//
optional<string>
- 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 <typename... A>
+ optional<string>
+ git_string (const semantic_version&, bool ignore_error, A&&... args);
+
+ template <typename... A>
+ optional<string>
+ git_string (const semantic_version&,
+ const dir_path& repo,
+ bool ignore_error,
+ A&&... args);
+
+ optional<string>
+ 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<A> (args)...);
}
+
+ template <typename... A>
+ inline optional<string>
+ git_string (const semantic_version& min_ver,
+ const dir_path& repo,
+ bool ie,
+ A&&... args)
+ {
+ return git_string (min_ver,
+ ie,
+ "-C", repo,
+ forward<A> (args)...);
+ }
+
+ inline optional<string>
+ 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 <typename... A>
optional<string>
- 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<A> (args)...));
- return git_line (move (pr), move (pipe), ie);
+ return git_line (move (pr), move (pipe), ie, delim);
+ }
+
+ template <typename... A>
+ inline optional<string>
+ git_line (const semantic_version& min_ver, bool ie, A&&... args)
+ {
+ return git_line (min_ver, ie, '\n' /* delim */, forward<A> (args)...);
+ }
+
+ template <typename... A>
+ inline optional<string>
+ git_string (const semantic_version& min_ver, bool ie, A&&... args)
+ {
+ return git_line (min_ver, ie, '\0' /* delim */, forward<A> (args)...);
}
template <typename... A>
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
+ {
+ "<num>",
+ "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<string> 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
+
}