Merge "config.md: Correct formatting of config values" into stable-2.16
diff --git a/.bazelrc b/.bazelrc
index f5c0ae3..615adcd 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -1,4 +1,4 @@
-build --workspace_status_command=./tools/workspace-status.sh
+build --workspace_status_command="python ./tools/workspace_status.py"
# Standalone build compatbility with npm requires PATH on action_env (Issue 10372).
build --action_env=PATH
diff --git a/.bazelversion b/.bazelversion
index 3eefcb9..227cea2 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-1.0.0
+2.0.0
diff --git a/WORKSPACE b/WORKSPACE
index a5e1670..839a68b 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -3,7 +3,7 @@
load("//:bazlets.bzl", "load_bazlets")
load_bazlets(
- commit = "ec989bb514e39447764057c60d3f9959bff8e153",
+ commit = "577450c73780560972f8a1a92c91410192ff7224",
#local_path = "/home/<user>/projects/bazlets",
)
@@ -16,9 +16,11 @@
gerrit_polymer()
# Load closure compiler with transitive dependencies
-load("@io_bazel_rules_closure//closure:defs.bzl", "closure_repositories")
+load("@io_bazel_rules_closure//closure:repositories.bzl", "rules_closure_dependencies", "rules_closure_toolchains")
-closure_repositories()
+rules_closure_dependencies()
+
+rules_closure_toolchains()
# Load Gerrit npm_binary toolchain
load("@com_googlesource_gerrit_bazlets//tools:js.bzl", "GERRIT", "npm_binary")
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteAction.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteAction.java
index 040f7e4..459d819 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteAction.java
@@ -16,7 +16,6 @@
import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.project.ProjectResource;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -37,8 +36,7 @@
DeleteLog deleteLog,
DeletePreconditions preConditions,
Configuration cfg,
- HideProject hideProject,
- NotesMigration migration) {
+ HideProject hideProject) {
super(
dbHandler,
fsHandler,
@@ -47,8 +45,7 @@
deleteLog,
preConditions,
cfg,
- hideProject,
- migration);
+ hideProject);
this.protectedProjects = protectedProjects;
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProject.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProject.java
index e3fa6b9..4b6e75f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProject.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProject.java
@@ -21,7 +21,6 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -50,7 +49,6 @@
private final DeleteLog deleteLog;
private final Configuration cfg;
private final HideProject hideProject;
- private NotesMigration migration;
@Inject
DeleteProject(
@@ -61,8 +59,7 @@
DeleteLog deleteLog,
DeletePreconditions preConditions,
Configuration cfg,
- HideProject hideProject,
- NotesMigration migration) {
+ HideProject hideProject) {
this.dbHandler = dbHandler;
this.fsHandler = fsHandler;
this.cacheHandler = cacheHandler;
@@ -71,7 +68,6 @@
this.preConditions = preConditions;
this.cfg = cfg;
this.hideProject = hideProject;
- this.migration = migration;
}
@Override
@@ -91,9 +87,7 @@
Exception ex = null;
try {
if (!preserve || !cfg.projectOnPreserveHidden()) {
- if (!migration.disableChangeReviewDb()) {
- dbHandler.delete(project);
- }
+ dbHandler.delete(project);
try {
fsHandler.delete(project, preserve);
} catch (RepositoryNotFoundException e) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/database/DatabaseDeleteHandler.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/database/DatabaseDeleteHandler.java
index 5dff8c5..cca8386 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/database/DatabaseDeleteHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/database/DatabaseDeleteHandler.java
@@ -15,6 +15,7 @@
package com.googlesource.gerrit.plugins.deleteproject.database;
import static java.util.Collections.singleton;
+import static java.util.stream.Collectors.toList;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.registration.DynamicItem;
@@ -30,7 +31,11 @@
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
import com.google.gerrit.server.change.AccountPatchReviewStore;
+import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeNotes.Factory.ChangeNotesResult;
+import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gwtorm.jdbc.JdbcSchema;
@@ -56,6 +61,9 @@
private final ChangeIndexer indexer;
private final Provider<InternalAccountQuery> accountQueryProvider;
private final Provider<AccountsUpdate> accountsUpdateProvider;
+ private final ChangeNotes.Factory schemaFactoryNoteDb;
+ private final GitRepositoryManager repoManager;
+ private final NotesMigration migration;
@Inject
public DatabaseDeleteHandler(
@@ -63,6 +71,9 @@
StarredChangesUtil starredChangesUtil,
DynamicItem<AccountPatchReviewStore> accountPatchReviewStore,
ChangeIndexer indexer,
+ ChangeNotes.Factory schemaFactoryNoteDb,
+ NotesMigration migration,
+ GitRepositoryManager repoManager,
Provider<InternalAccountQuery> accountQueryProvider,
@UserInitiated Provider<AccountsUpdate> accountsUpdateProvider) {
this.dbProvider = dbProvider;
@@ -71,29 +82,40 @@
this.indexer = indexer;
this.accountQueryProvider = accountQueryProvider;
this.accountsUpdateProvider = accountsUpdateProvider;
+ this.schemaFactoryNoteDb = schemaFactoryNoteDb;
+ this.repoManager = repoManager;
+ this.migration = migration;
}
- public void delete(Project project) throws OrmException {
+ public void delete(Project project) throws OrmException, IOException {
ReviewDb db = ReviewDbUtil.unwrapDb(dbProvider.get());
- Connection conn = ((JdbcSchema) db).getConnection();
- try {
- conn.setAutoCommit(false);
+ if (isReviewDb()) {
+ Connection conn = ((JdbcSchema) db).getConnection();
try {
- atomicDelete(db, project, getChangesList(project, conn));
- conn.commit();
- } finally {
- conn.setAutoCommit(true);
+ conn.setAutoCommit(false);
+ try {
+ atomicDelete(db, project, getChangesList(project, conn));
+ conn.commit();
+ } finally {
+ conn.setAutoCommit(true);
+ }
+ } catch (SQLException e) {
+ try {
+ conn.rollback();
+ } catch (SQLException ex) {
+ throw new OrmException(ex);
+ }
+ throw new OrmException(e);
}
- } catch (SQLException e) {
- try {
- conn.rollback();
- } catch (SQLException ex) {
- throw new OrmException(ex);
- }
- throw new OrmException(e);
+ } else {
+ atomicDelete(db, project, getChangesListFromNoteDb(project));
}
}
+ private boolean isReviewDb() {
+ return !migration.disableChangeReviewDb();
+ }
+
private List<Change.Id> getChangesList(Project project, Connection conn) throws SQLException {
try (PreparedStatement changesForProject =
conn.prepareStatement("SELECT change_id FROM changes WHERE dest_project_name = ?")) {
@@ -110,6 +132,20 @@
}
}
+ private List<Change.Id> getChangesListFromNoteDb(Project project) throws IOException {
+ Project.NameKey projectKey = project.getNameKey();
+ List<Change.Id> changeIds =
+ schemaFactoryNoteDb
+ .scan(repoManager.openRepository(projectKey), dbProvider.get(), projectKey)
+ .map(ChangeNotesResult::id)
+ .collect(toList());
+ log.atFine().log(
+ "Number of changes in noteDb related to project %s are %d",
+ projectKey.get(),
+ changeIds.size());
+ return changeIds;
+ }
+
private void deleteChanges(ReviewDb db, Project.NameKey project, List<Change.Id> changeIds)
throws OrmException {
@@ -119,18 +155,19 @@
} catch (NoSuchChangeException e) {
// we can ignore the exception during delete
}
- ResultSet<PatchSet> patchSets = db.patchSets().byChange(id);
- if (patchSets != null) {
- deleteFromPatchSets(db, patchSets);
+ if (isReviewDb()) {
+ ResultSet<PatchSet> patchSets = db.patchSets().byChange(id);
+ if (patchSets != null) {
+ deleteFromPatchSets(db, patchSets);
+ }
+
+ // In the future, use schemaVersion to decide what to delete.
+ db.patchComments().delete(db.patchComments().byChange(id));
+ db.patchSetApprovals().delete(db.patchSetApprovals().byChange(id));
+
+ db.changeMessages().delete(db.changeMessages().byChange(id));
+ db.changes().deleteKeys(Collections.singleton(id));
}
-
- // In the future, use schemaVersion to decide what to delete.
- db.patchComments().delete(db.patchComments().byChange(id));
- db.patchSetApprovals().delete(db.patchSetApprovals().byChange(id));
-
- db.changeMessages().delete(db.changeMessages().byChange(id));
- db.changes().deleteKeys(Collections.singleton(id));
-
// Delete from the secondary index
try {
indexer.delete(id);
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
index b47afaa..a61ad8c 100644
--- a/src/main/resources/Documentation/build.md
+++ b/src/main/resources/Documentation/build.md
@@ -68,7 +68,7 @@
To execute the tests run:
```
- bazel test plugins/@PLUGIN@:delete_project_tests
+ bazel test plugins/@PLUGIN@:delete-project_tests
```
or filtering using the comma separated tags:
diff --git a/src/main/resources/static/delete-project-with-preserve-disabled.js b/src/main/resources/static/delete-project-with-preserve-disabled.js
index 550ea6c..2065616 100644
--- a/src/main/resources/static/delete-project-with-preserve-disabled.js
+++ b/src/main/resources/static/delete-project-with-preserve-disabled.js
@@ -12,31 +12,44 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-if (!window.Polymer) {
+function _getCookie(name) {
+ var key = name + '=';
+ var result = '';
+ document.cookie.split(';').some(c => {
+ c = c.trim();
+ if (c.indexOf(key, 0) === 0) {
+ result = c.substring(key.length);
+ return true;
+ }
+ });
+ return result;
+}
+
+if (_getCookie('GERRIT_UI') === 'GWT') {
Gerrit.install(function(self) {
- function onDeleteProject(c) {
- var f = c.checkbox();
- var b = c.button('Delete',
- {onclick: function(){
- c.call(
- {force: f.checked, preserve: false},
- function(r) {
- c.hide();
- window.alert('The project: "'
- + c.project
- + '" was deleted.'),
- Gerrit.go('/admin/projects/');
- });
- }});
- c.popup(c.div(
- c.msg('Are you really sure you want to delete the project: "'
- + c.project
- + '"?'),
- c.br(),
- c.label(f, 'Delete project even if open changes exist?'),
- c.br(),
- b));
- }
- self.onAction('project', 'delete', onDeleteProject);
- });
-}
\ No newline at end of file
+ function onDeleteProject(c) {
+ var f = c.checkbox();
+ var b = c.button('Delete',
+ {onclick: function(){
+ c.call(
+ {force: f.checked, preserve: false},
+ function(r) {
+ c.hide();
+ window.alert('The project: "'
+ + c.project
+ + '" was deleted.'),
+ Gerrit.go('/admin/projects/');
+ });
+ }});
+ c.popup(c.div(
+ c.msg('Are you really sure you want to delete the project: "'
+ + c.project
+ + '"?'),
+ c.br(),
+ c.label(f, 'Delete project even if open changes exist?'),
+ c.br(),
+ b));
+ }
+ self.onAction('project', 'delete', onDeleteProject);
+ });
+}
diff --git a/src/main/resources/static/delete-project.js b/src/main/resources/static/delete-project.js
index 2291279..a72ad60 100644
--- a/src/main/resources/static/delete-project.js
+++ b/src/main/resources/static/delete-project.js
@@ -12,34 +12,47 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-if (!window.Polymer) {
+function _getCookie(name) {
+ var key = name + '=';
+ var result = '';
+ document.cookie.split(';').some(c => {
+ c = c.trim();
+ if (c.indexOf(key, 0) === 0) {
+ result = c.substring(key.length);
+ return true;
+ }
+ });
+ return result;
+}
+
+if (_getCookie('GERRIT_UI') === 'GWT') {
Gerrit.install(function(self) {
- function onDeleteProject(c) {
- var f = c.checkbox();
- var p = c.checkbox();
- var b = c.button('Delete',
- {onclick: function(){
- c.call(
- {force: f.checked, preserve: p.checked},
- function(r) {
- c.hide();
- window.alert('The project: "'
- + c.project
- + '" was deleted.'),
- Gerrit.go('/admin/projects/');
- });
- }});
- c.popup(c.div(
- c.msg('Are you really sure you want to delete the project: "'
- + c.project
- + '"?'),
- c.br(),
- c.label(f, 'Delete project even if open changes exist?'),
- c.br(),
- c.label(p, 'Preserve GIT Repository?'),
- c.br(),
- b));
- }
- self.onAction('project', 'delete', onDeleteProject);
- });
+ function onDeleteProject(c) {
+ var f = c.checkbox();
+ var p = c.checkbox();
+ var b = c.button('Delete',
+ {onclick: function(){
+ c.call(
+ {force: f.checked, preserve: p.checked},
+ function(r) {
+ c.hide();
+ window.alert('The project: "'
+ + c.project
+ + '" was deleted.'),
+ Gerrit.go('/admin/projects/');
+ });
+ }});
+ c.popup(c.div(
+ c.msg('Are you really sure you want to delete the project: "'
+ + c.project
+ + '"?'),
+ c.br(),
+ c.label(f, 'Delete project even if open changes exist?'),
+ c.br(),
+ c.label(p, 'Preserve GIT Repository?'),
+ c.br(),
+ b));
+ }
+ self.onAction('project', 'delete', onDeleteProject);
+ });
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeletePreconditionsTest.java b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeletePreconditionsTest.java
index 6658478..1074e65 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeletePreconditionsTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeletePreconditionsTest.java
@@ -15,6 +15,7 @@
package com.googlesource.gerrit.plugins.deleteproject;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static com.googlesource.gerrit.plugins.deleteproject.DeleteOwnProjectCapability.DELETE_OWN_PROJECT;
import static com.googlesource.gerrit.plugins.deleteproject.DeleteProjectCapability.DELETE_PROJECT;
import static org.mockito.Mockito.doNothing;
@@ -43,9 +44,7 @@
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
@@ -68,8 +67,6 @@
@Mock private PermissionBackend permissionBackend;
@Mock private PermissionBackend.WithUser userPermission;
- @Rule public ExpectedException expectedException = ExpectedException.none();
-
private ProjectResource rsrc;
private DeletePreconditions preConditions;
@@ -121,16 +118,17 @@
@Test
public void testUserCannotDelete() throws Exception {
when(permissionBackend.user(currentUser)).thenReturn(userPermission);
- expectedException.expect(AuthException.class);
- expectedException.expectMessage("not allowed to delete project");
- preConditions.assertDeletePermission(rsrc);
+ AuthException thrown =
+ assertThrows(AuthException.class, () -> preConditions.assertDeletePermission(rsrc));
+ assertThat(thrown).hasMessageThat().contains("not allowed to delete project");
}
@Test
public void testIsProtectedSoCannotBeDeleted() throws Exception {
doThrow(CannotDeleteProjectException.class).when(protectedProjects).assertIsNotProtected(rsrc);
- expectedException.expect(ResourceConflictException.class);
- preConditions.assertCanBeDeleted(rsrc, new DeleteProject.Input());
+ assertThrows(
+ ResourceConflictException.class,
+ () -> preConditions.assertCanBeDeleted(rsrc, new DeleteProject.Input()));
}
@Test
@@ -140,9 +138,13 @@
when(listChildProjectsProvider.get()).thenReturn(childProjects);
when(childProjects.withLimit(1)).thenReturn(childProjects);
when(childProjects.apply(rsrc)).thenReturn(ImmutableList.of(new ProjectInfo()));
- expectedException.expect(ResourceConflictException.class);
- expectedException.expectMessage("Cannot delete project because it has at least one child:");
- preConditions.assertCanBeDeleted(rsrc, new DeleteProject.Input());
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> preConditions.assertCanBeDeleted(rsrc, new DeleteProject.Input()));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("Cannot delete project because it has at least one child:");
}
@Test
@@ -152,9 +154,11 @@
when(queryChange.byProjectOpen(PROJECT_NAMEKEY)).thenReturn(ImmutableList.of(cd));
when(queryProvider.get()).thenReturn(queryChange);
String expectedMessage = String.format("Project '%s' has open changes.", PROJECT_NAMEKEY.get());
- expectedException.expectMessage(expectedMessage);
- expectedException.expect(CannotDeleteProjectException.class);
- preConditions.assertHasOpenChanges(PROJECT_NAMEKEY, false);
+ CannotDeleteProjectException thrown =
+ assertThrows(
+ CannotDeleteProjectException.class,
+ () -> preConditions.assertHasOpenChanges(PROJECT_NAMEKEY, false));
+ assertThat(thrown).hasMessageThat().contains(expectedMessage);
}
@Test
@@ -164,8 +168,10 @@
when(queryProvider.get()).thenReturn(queryChange);
String expectedMessage =
String.format("Unable to verify if '%s' has open changes.", PROJECT_NAMEKEY.get());
- expectedException.expectMessage(expectedMessage);
- expectedException.expect(CannotDeleteProjectException.class);
- preConditions.assertHasOpenChanges(PROJECT_NAMEKEY, false);
+ CannotDeleteProjectException thrown =
+ assertThrows(
+ CannotDeleteProjectException.class,
+ () -> preConditions.assertHasOpenChanges(PROJECT_NAMEKEY, false));
+ assertThat(thrown).hasMessageThat().contains(expectedMessage);
}
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProjectIT.java b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProjectIT.java
index 206f231..36d1ab3 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProjectIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProjectIT.java
@@ -28,9 +28,11 @@
import com.google.gerrit.acceptance.UseSsh;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gwtorm.server.OrmException;
import com.googlesource.gerrit.plugins.deleteproject.DeleteProject.Input;
import java.io.File;
import java.io.IOException;
@@ -79,6 +81,7 @@
RestResponse r = httpDeleteProjectHelper(true);
r.assertNoContent();
assertThat(projectDir.exists()).isFalse();
+ assertAllChangesDeletedInIndex();
}
@Test
@@ -97,6 +100,8 @@
RestResponse r = httpDeleteProjectHelper(true);
r.assertNoContent();
assertThat(projectDir.exists()).isFalse();
+ assertAllChangesDeletedInIndex();
+ assertWatchRemoved();
}
@Test
@@ -139,6 +144,19 @@
adminSshSession.exec(cmd);
assertThat(adminSshSession.getError()).isNull();
assertThat(projectDir.exists()).isFalse();
+ assertAllChangesDeletedInIndex();
+ }
+
+ @Test
+ @UseLocalDisk
+ public void testSshDeleteProjectWithWatches() throws Exception {
+ watch(project.get());
+ String cmd = createDeleteCommand(project.get());
+ adminSshSession.exec(cmd);
+ assertThat(adminSshSession.getError()).isNull();
+ assertThat(projectDir.exists()).isFalse();
+ assertAllChangesDeletedInIndex();
+ assertWatchRemoved();
}
@Test
@@ -237,6 +255,7 @@
assertThat(isEmpty(archiveFolder.toPath())).isFalse();
assertThat(containsDeletedProject(archiveFolder.toPath(), project.get())).isTrue();
assertThat(projectDir.exists()).isFalse();
+ assertAllChangesDeletedInIndex();
}
@Test
@@ -268,7 +287,7 @@
assertThat(containsDeletedProject(archiveFolder.toPath().resolve(PARENT_FOLDER), name))
.isTrue();
assertThat(projectDir.exists()).isFalse();
-
+ assertAllChangesDeletedInIndex();
assertThat(parentFolder.toFile().exists()).isFalse();
}
@@ -321,4 +340,12 @@
return dirStream.anyMatch(d -> d.toString().contains(projectName));
}
}
+
+ private void assertAllChangesDeletedInIndex() throws OrmException {
+ assertThat(queryProvider.get().byProject(project)).isEmpty();
+ }
+
+ private void assertWatchRemoved() throws RestApiException {
+ assertThat(gApi.accounts().self().getWatchedProjects()).isEmpty();
+ }
}
diff --git a/tools/workspace-status.sh b/tools/workspace-status.sh
deleted file mode 100755
index 3185cae..0000000
--- a/tools/workspace-status.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/bash
-
-# This script will be run by bazel when the build 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.
-
-function rev() {
- cd $1; git describe --always --match "v[0-9].*" --dirty
-}
-
-echo STABLE_BUILD_DELETE-PROJECT_LABEL $(rev .)
diff --git a/tools/workspace_status.py b/tools/workspace_status.py
new file mode 100644
index 0000000..e65db86
--- /dev/null
+++ b/tools/workspace_status.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+
+# This script will be run by bazel when the build 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.
+
+from __future__ import print_function
+import subprocess
+import sys
+
+CMD = ['git', 'describe', '--always', '--match', 'v[0-9].*', '--dirty']
+
+
+def revision():
+ try:
+ return subprocess.check_output(CMD).strip().decode("utf-8")
+ except OSError as err:
+ print('could not invoke git: %s' % err, file=sys.stderr)
+ sys.exit(1)
+ except subprocess.CalledProcessError as err:
+ print('error using git: %s' % err, file=sys.stderr)
+ sys.exit(1)
+
+
+print("STABLE_BUILD_DELETE-PROJECT_LABEL %s" % revision())