From 884b16c882863f1eb5144a4bf7d1739bdf99a271 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Thu, 13 Dec 2018 12:43:27 +0200 Subject: Factor git-status code from bdep-ci to git_status() --- bdep/ci.cxx | 125 +++++++++++++---------------------------------------------- bdep/git.cxx | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ bdep/git.hxx | 22 +++++++++++ 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 | (initial) Current commit. - // # branch.head | (detached) Current branch. - // # branch.upstream If upstream is set. - // # branch.ab + - 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 / 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 / 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 | (initial) Current commit. + // # branch.head | (detached) Current branch. + // # branch.upstream If upstream is set. + // # branch.ab + - 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 + and - 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 ... + // 2 ... + // + // Where 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 / 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 -- cgit