summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBoris Kolpackov <boris@codesynthesis.com>2018-12-13 12:43:27 +0200
committerBoris Kolpackov <boris@codesynthesis.com>2018-12-13 12:43:27 +0200
commit884b16c882863f1eb5144a4bf7d1739bdf99a271 (patch)
tree8d5aeb8b318cfdc99e2315168e0cd9d232a92c78
parent213a905803f069b7d3148cac8591f4046325bad0 (diff)
downloadbdep-884b16c882863f1eb5144a4bf7d1739bdf99a271.tar.gz
bdep-884b16c882863f1eb5144a4bf7d1739bdf99a271.tar.xz
bdep-884b16c882863f1eb5144a4bf7d1739bdf99a271.zip
Factor git-status code from bdep-ci to git_status()
-rw-r--r--bdep/ci.cxx125
-rw-r--r--bdep/git.cxx119
-rw-r--r--bdep/git.hxx22
3 files changed, 169 insertions, 97 deletions
diff --git a/bdep/ci.cxx b/bdep/ci.cxx
index 0ab6fad..83119c1 100644
--- a/bdep/ci.cxx
+++ b/bdep/ci.cxx
@@ -44,112 +44,43 @@ namespace bdep
//
// 4. Get the current commit id.
//
- // And aren't we in luck today: git-status --porcelain=2 (available since
- // git 2.11.0) gives us all this information with a single invocation.
- //
string branch;
string commit;
{
- string head;
- string upstream;
+ git_repository_status s (git_status (prj));
- process pr;
- bool io (false);
- try
- {
- fdpipe pipe (fdopen_pipe ()); // Text mode seems appropriate.
-
- pr = start_git (semantic_version {2, 11, 0},
- prj,
- 0 /* stdin */,
- pipe /* stdout */,
- 2 /* stderr */,
- "status",
- "--porcelain=2",
- "--branch");
-
- pipe.out.close ();
- ifdstream is (move (pipe.in), fdstream_mode::skip, ifdstream::badbit);
-
- // Lines starting with '#' are headers with any other line indicating
- // some kind of change.
- //
- // The headers we are interested in are:
- //
- // # branch.oid <commit> | (initial) Current commit.
- // # branch.head <branch> | (detached) Current branch.
- // # branch.upstream <upstream_branch> If upstream is set.
- // # branch.ab +<ahead> -<behind> If upstream is set and
- // the commit is present.
- //
- // Note that if we are in the detached HEAD state, then we will only
- // see the first two with branch.head being '(detached)'.
- //
- for (string l; !eof (getline (is, l)); )
- {
- if (l[0] != '#')
- fail << "project directory has uncommitted changes" <<
- info << "run 'git status' for details";
-
- if (l.compare (2, 10, "branch.oid") == 0)
- {
- commit = string (l, 13);
-
- if (commit == "(initial)")
- fail << "no commits in project repository" <<
- info << "run 'git status' for details";
- }
- else if (l.compare (2, 11, "branch.head") == 0)
- {
- head = string (l, 14);
-
- if (head == "(detached)")
- fail << "project directory is in the detached HEAD state" <<
- info << "run 'git status' for details";
- }
- else if (l.compare (2, 15, "branch.upstream") == 0)
- {
- // This is normally in the <remote>/<branch> form, for example
- // 'origin/master'.
- //
- upstream = string (l, 18);
- size_t p (path::traits::rfind_separator (upstream));
- branch = p != string::npos ? string (upstream, p + 1) : upstream;
- }
- else if (l.compare (2, 9, "branch.ab") == 0)
- {
- // We definitely don't want to be ahead (upstream doesn't have
- // this commit) but there doesn't seem be anything wrong with
- // being behind.
- //
- if (l.compare (12, 3, "+0 ") != 0)
- fail << "local branch '" << head << "' is ahead of '"
- << upstream << "'" <<
- info << "run 'git push' to update";
- }
- }
-
- is.close (); // Detect errors.
- }
- catch (const io_error&)
- {
- // Presumably the child process failed and issued diagnostics so let
- // finish_git() try to deal with that.
- //
- io = true;
- }
+ if (s.commit.empty ())
+ fail << "no commits in project repository" <<
+ info << "run 'git status' for details";
- finish_git (pr, io);
+ commit = move (s.commit);
- // Make sure we've got everything we need.
- //
- if (commit.empty ())
- fail << "unable to obtain current commit" <<
+ if (s.branch.empty ())
+ fail << "project directory is in the detached HEAD state" <<
info << "run 'git status' for details";
- if (branch.empty ())
- fail << "no upstream branch set for local branch '" << head << "'" <<
+ // Upstream is normally in the <remote>/<branch> form, for example
+ // 'origin/master'.
+ //
+ if (s.upstream.empty ())
+ fail << "no upstream branch set for local branch '"
+ << s.branch << "'" <<
info << "run 'git push --set-upstream' to set";
+
+ size_t p (path::traits::rfind_separator (s.upstream));
+ branch = p != string::npos ? string (s.upstream, p + 1) : s.upstream;
+
+ if (s.staged || s.unstaged)
+ fail << "project directory has uncommitted changes" <<
+ info << "run 'git status' for details";
+
+ // We definitely don't want to be ahead (upstream doesn't have this
+ // commit) but there doesn't seem be anything wrong with being behind.
+ //
+ if (s.ahead)
+ fail << "local branch '" << s.branch << "' is ahead of '"
+ << s.upstream << "'" <<
+ info << "run 'git push' to update";
}
// We treat the URL specified with --repository as a "base", that is, we
diff --git a/bdep/git.cxx b/bdep/git.cxx
index e9b1eba..2a81550 100644
--- a/bdep/git.cxx
+++ b/bdep/git.cxx
@@ -248,4 +248,123 @@ namespace bdep
fail << "unable to discover " << what << ": no git remote.origin.url "
<< "value" << endf;
}
+
+ git_repository_status
+ git_status (const dir_path& repo)
+ {
+ git_repository_status r;
+
+ // git-status --porcelain=2 (available since git 2.11.0) gives us all the
+ // information with a single invocation.
+ //
+ process pr;
+ bool io (false);
+ try
+ {
+ fdpipe pipe (fdopen_pipe ()); // Text mode seems appropriate.
+
+ pr = start_git (semantic_version {2, 11, 0},
+ repo,
+ 0 /* stdin */,
+ pipe /* stdout */,
+ 2 /* stderr */,
+ "status",
+ "--porcelain=2",
+ "--branch");
+
+ pipe.out.close ();
+ ifdstream is (move (pipe.in), fdstream_mode::skip, ifdstream::badbit);
+
+ // Lines starting with '#' are headers (come first) with any other line
+ // indicating some kind of change.
+ //
+ // The headers we are interested in are:
+ //
+ // # branch.oid <commit> | (initial) Current commit.
+ // # branch.head <branch> | (detached) Current branch.
+ // # branch.upstream <upstream_branch> If upstream is set.
+ // # branch.ab +<ahead> -<behind> If upstream is set and
+ // the commit is present.
+ //
+ // Note that if we are in the detached HEAD state, then we will only
+ // see the first two with branch.head being '(detached)'.
+ //
+ for (string l; !eof (getline (is, l)); )
+ {
+ char c (l[0]);
+
+ if (c == '#')
+ {
+ if (l.compare (2, 10, "branch.oid") == 0)
+ {
+ r.commit = string (l, 13);
+
+ if (r.commit == "(initial)")
+ r.commit.clear ();
+ }
+ else if (l.compare (2, 11, "branch.head") == 0)
+ {
+ r.branch = string (l, 14);
+
+ if (r.branch == "(detached)")
+ r.branch.clear ();
+ }
+ else if (l.compare (2, 15, "branch.upstream") == 0)
+ {
+ r.upstream = string (l, 18);
+ }
+ else if (l.compare (2, 9, "branch.ab") == 0)
+ {
+ // Both +<ahead> and -<behind> are always present, even if 0.
+ //
+ size_t a (l.find ('+', 12)); assert (a != string::npos);
+ size_t b (l.find ('-', 12)); assert (b != string::npos);
+
+ if (l[a + 1] != '0') r.ahead = true;
+ if (l[b + 1] != '0') r.behind = true;
+ }
+
+ continue; // Some other header.
+ }
+
+ // Change line. For tracked entries it has the following format:
+ //
+ // 1 <XY> ...
+ // 2 <XY> ...
+ //
+ // Where <XY> is a two-character field with X describing the staged
+ // status and Y -- unstaged and with '.' indicating no change.
+ //
+ // All other lines (untracked/unmerged entries) we treat as an
+ // indication of an unstaged change (see git-status(1) for details).
+ //
+ if (c == '1' || c == '2')
+ {
+ if (l[2] != '.') r.staged = true;
+ if (l[3] != '.') r.unstaged = true;
+ }
+ else
+ r.unstaged = true;
+
+ // Skip the rest if we already know the outcome (remember, headers
+ // always come first).
+ //
+ if (r.staged && r.unstaged)
+ break;
+ }
+
+ is.close (); // Detect errors.
+ }
+ catch (const io_error&)
+ {
+ // Presumably the child process failed and issued diagnostics so let
+ // finish_git() try to deal with that.
+ //
+ io = true;
+ }
+
+ finish_git (pr, io);
+
+ return r;
+ }
}
diff --git a/bdep/git.hxx b/bdep/git.hxx
index 8ef9397..91a9fd9 100644
--- a/bdep/git.hxx
+++ b/bdep/git.hxx
@@ -71,6 +71,28 @@ namespace bdep
const char* opt = nullptr,
const char* what = "remote repository URL",
const char* cfg = nullptr);
+
+ // Repository status.
+ //
+ struct git_repository_status
+ {
+ string commit; // Current commit or empty if initial.
+ string branch; // Local branch or empty if detached.
+ string upstream; // Upstream in <remote>/<branch> form or empty if not set.
+
+ // Note that unmerged and untracked entries are considered as unstaged.
+ //
+ bool staged = false; // Repository has staged changes.
+ bool unstaged = false; // Repository has unstaged changes.
+
+ // Note that we can be both ahead and behind.
+ //
+ bool ahead = false; // Local branch is ahead of upstream.
+ bool behind = false; // Local branch is behind of upstream.
+ };
+
+ git_repository_status
+ git_status (const dir_path& repo);
}
#include <bdep/git.ixx>