Introduce hook to BazelWorkspaceStatusModule that calls external
script(controled by workspace_status_command option, default to
tools/buildstamp/get_workspace_status) to emit addtional workspace
information to stable-status.txt.

This should address #216.

--
Change-Id: Iffb06482489f0d55393e27b0764e6e127fedbc20
Reviewed-on: https://bazel-review.git.corp.google.com/#/c/1550
MOS_MIGRATED_REVID=97678871
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BuildInfo.java b/src/main/java/com/google/devtools/build/lib/analysis/BuildInfo.java
index 65cdbf6..2972e46 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/BuildInfo.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BuildInfo.java
@@ -38,4 +38,14 @@
    * Build time as milliseconds since epoch
    */
   public static final String BUILD_TIMESTAMP = "BUILD_TIMESTAMP";
+
+  /**
+   * The revision of source tree reported by source control system
+   */
+  public static final String BUILD_SCM_REVISION = "BUILD_SCM_REVISION";
+
+  /**
+   * The status of source tree reported by source control system
+   */
+  public static final String BUILD_SCM_STATUS = "BUILD_SCM_STATUS";
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BazelWorkspaceStatusModule.java b/src/main/java/com/google/devtools/build/lib/bazel/BazelWorkspaceStatusModule.java
index 23b6b1e..919a001 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/BazelWorkspaceStatusModule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/BazelWorkspaceStatusModule.java
@@ -34,10 +34,14 @@
 import com.google.devtools.build.lib.analysis.WorkspaceStatusAction;
 import com.google.devtools.build.lib.analysis.WorkspaceStatusAction.Key;
 import com.google.devtools.build.lib.analysis.WorkspaceStatusAction.KeyType;
+import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.runtime.BlazeModule;
 import com.google.devtools.build.lib.runtime.BlazeRuntime;
 import com.google.devtools.build.lib.runtime.Command;
 import com.google.devtools.build.lib.runtime.GotOptionsEvent;
+import com.google.devtools.build.lib.shell.CommandException;
+import com.google.devtools.build.lib.shell.CommandResult;
+import com.google.devtools.build.lib.util.CommandBuilder;
 import com.google.devtools.build.lib.util.NetUtil;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -59,13 +63,16 @@
     private final Artifact stableStatus;
     private final Artifact volatileStatus;
     private final AtomicReference<Options> options;
-    
+
     private final String username;
     private final String hostname;
     private final long timestamp;
+    private final com.google.devtools.build.lib.shell.Command getWorkspaceStatusCommand;
+    private final PathFragment EMPTY_FRAGMENT = new PathFragment("");
 
     private BazelWorkspaceStatusAction(
         AtomicReference<WorkspaceStatusAction.Options> options,
+        BlazeRuntime runtime,
         Artifact stableStatus,
         Artifact volatileStatus) {
       super(BuildInfoHelper.BUILD_INFO_ACTION_OWNER, Artifact.NO_ARTIFACTS,
@@ -76,6 +83,18 @@
       this.username = System.getProperty("user.name");
       this.hostname = NetUtil.findShortHostName();
       this.timestamp = System.currentTimeMillis();
+      this.getWorkspaceStatusCommand =
+          options.get().workspaceStatusCommand.equals(EMPTY_FRAGMENT)
+              ? null
+              : new CommandBuilder()
+                  .addArgs(options.get().workspaceStatusCommand.toString())
+                  // Pass client env, because certain SCM client(like
+                  // perforce, git) relies on environment variables to work
+                  // correctly.
+                  .setEnv(runtime.getClientEnv())
+                  .setWorkingDir(runtime.getWorkspace())
+                  .useShell(true)
+                  .build();
     }
 
     @Override
@@ -83,6 +102,30 @@
       return "";
     }
 
+    private String getAdditionalWorkspaceStatus(ActionExecutionContext actionExecutionContext)
+        throws ActionExecutionException {
+      try {
+        if (this.getWorkspaceStatusCommand != null) {
+          actionExecutionContext
+              .getExecutor()
+              .getEventHandler()
+              .handle(
+                  Event.progress(
+                      "Getting additional workspace status by running "
+                          + options.get().workspaceStatusCommand));
+          CommandResult result = this.getWorkspaceStatusCommand.execute();
+          if (result.getTerminationStatus().success()) {
+            return new String(result.getStdout());
+          }
+          throw new ActionExecutionException(
+              "workspace status command failed: " + result.getTerminationStatus(), this, true);
+        }
+      } catch (CommandException e) {
+        throw new ActionExecutionException(e, this, true);
+      }
+      return "";
+    }
+
     @Override
     public void execute(ActionExecutionContext actionExecutionContext)
         throws ActionExecutionException {
@@ -94,12 +137,19 @@
                 BuildInfo.BUILD_HOST + " " + hostname,
                 BuildInfo.BUILD_USER + " " + username);
         FileSystemUtils.writeContent(stableStatus.getPath(), info.getBytes(StandardCharsets.UTF_8));
-        String volatileInfo = BuildInfo.BUILD_TIMESTAMP + " " + timestamp + "\n";
+        String volatileInfo =
+            joiner.join(
+                BuildInfo.BUILD_TIMESTAMP + " " + timestamp,
+                getAdditionalWorkspaceStatus(actionExecutionContext));
 
         FileSystemUtils.writeContent(
             volatileStatus.getPath(), volatileInfo.getBytes(StandardCharsets.UTF_8));
       } catch (IOException e) {
-        throw new ActionExecutionException(e, this, true);
+        throw new ActionExecutionException(
+            "Failed to run workspace status command " + options.get().workspaceStatusCommand,
+            e,
+            this,
+            true);
       }
     }
 
@@ -172,7 +222,7 @@
       Artifact volatileArtifact = factory.getConstantMetadataArtifact(
           new PathFragment("volatile-status.txt"), root, artifactOwner);
 
-      return new BazelWorkspaceStatusAction(options, stableArtifact, volatileArtifact);
+      return new BazelWorkspaceStatusAction(options, runtime, stableArtifact, volatileArtifact);
     }
   }
 
@@ -186,7 +236,11 @@
           BuildInfo.BUILD_HOST,
           Key.of(KeyType.STRING, "hostname", "redacted"),
           BuildInfo.BUILD_USER,
-          Key.of(KeyType.STRING, "username", "redacted"));
+          Key.of(KeyType.STRING, "username", "redacted"),
+          BuildInfo.BUILD_SCM_REVISION,
+          Key.of(KeyType.STRING, "0", "0"),
+          BuildInfo.BUILD_SCM_STATUS,
+          Key.of(KeyType.STRING, "", "redacted"));
     }
 
     @Override
diff --git a/tools/buildstamp/BUILD b/tools/buildstamp/BUILD
new file mode 100644
index 0000000..6ad7e59
--- /dev/null
+++ b/tools/buildstamp/BUILD
@@ -0,0 +1,8 @@
+package(default_visibility = ["//visibility:public"])
+
+filegroup(
+    name = "all",
+    srcs = [
+        "get_workspace_status",
+    ],
+)
diff --git a/tools/buildstamp/get_workspace_status b/tools/buildstamp/get_workspace_status
new file mode 100644
index 0000000..1c358f7
--- /dev/null
+++ b/tools/buildstamp/get_workspace_status
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+# This script will be run bazel when building process starts to
+# generate key-value information that represents the status of the
+# workspace. The output should be like
+#
+# KEY1 VALUE1
+# KEY2 VALUE2
+#
+# If the script exits with non-zero code, it's considered as a failure
+# and the output will be discarded.
+
+# The code below presents an implementation that works for git repository
+git_rev=$(git rev-parse HEAD)
+if [[ $? != 0 ]];
+then
+    exit 1
+fi
+echo "BUILD_SCM_REVISION ${git_rev}"
+
+# Check whether there are any uncommited changes
+git diff-index --quiet HEAD --
+if [[ $? == 0 ]];
+then
+    tree_status="Clean"
+else
+    tree_status="Modified"
+fi
+echo "BUILD_SCM_STATUS ${tree_status}"