diff --git a/saved_state/LocalSavedStateInterface.cpp b/saved_state/LocalSavedStateInterface.cpp index 52ace66ddd83..da446f3a457c 100644 --- a/saved_state/LocalSavedStateInterface.cpp +++ b/saved_state/LocalSavedStateInterface.cpp @@ -80,6 +80,12 @@ LocalSavedStateInterface::getMostRecentSavedStateImpl( } w_string LocalSavedStateInterface::getLocalPath(w_string_piece commitId) const { - return w_string::pathCat({localStoragePath_, project_, commitId}); + w_string filename; + if (!projectMetadata_) { + filename = w_string::build(commitId); + } else { + filename = w_string::build(commitId, w_string("_"), projectMetadata_); + } + return w_string::pathCat({localStoragePath_, project_, filename}); } } // namespace watchman diff --git a/saved_state/LocalSavedStateInterface.h b/saved_state/LocalSavedStateInterface.h index a7a953bba87e..cd84dc74894b 100644 --- a/saved_state/LocalSavedStateInterface.h +++ b/saved_state/LocalSavedStateInterface.h @@ -10,7 +10,10 @@ namespace watchman { // stored on the local filesystem. The local storage path must contain a // subdirectory for the project, and within the project directory the saved // state for a given commit must be in a file whose name is the source control -// commit hash. +// commit hash. If project metadata is not specified, then only saved states +// with no project metadata will be returned. If project metadata is specified, +// then the most recent saved state with the specified project metadata will be +// returned. // // Checks the most recent n commits to find a saved state, if available. If a // saved state is not available, returns an error message in the saved state diff --git a/saved_state/SavedStateInterface.cpp b/saved_state/SavedStateInterface.cpp index f6fd6fb3015b..62fbb09b3312 100644 --- a/saved_state/SavedStateInterface.cpp +++ b/saved_state/SavedStateInterface.cpp @@ -39,6 +39,15 @@ SavedStateInterface::SavedStateInterface(const json_ref& savedStateConfig) { throw QueryParseError("'project' must be a string"); } project_ = json_to_w_string(project); + auto projectMetadata = savedStateConfig.get_default("project-metadata"); + if (projectMetadata) { + if (!projectMetadata.isString()) { + throw QueryParseError("'project-metadata' must be a string"); + } + projectMetadata_ = json_to_w_string(projectMetadata); + } else { + projectMetadata_ = w_string(); + } } SavedStateInterface::SavedStateResult diff --git a/saved_state/SavedStateInterface.h b/saved_state/SavedStateInterface.h index a9d3b5815d4f..efa4fd62bafc 100644 --- a/saved_state/SavedStateInterface.h +++ b/saved_state/SavedStateInterface.h @@ -46,6 +46,7 @@ class SavedStateInterface { protected: w_string project_; + w_string projectMetadata_; explicit SavedStateInterface(const json_ref& savedStateConfig); virtual SavedStateResult getMostRecentSavedStateImpl( diff --git a/tests/LocalSavedStateInterfaceTest.cpp b/tests/LocalSavedStateInterfaceTest.cpp index fe28548304c1..d47b45bbceba 100644 --- a/tests/LocalSavedStateInterfaceTest.cpp +++ b/tests/LocalSavedStateInterfaceTest.cpp @@ -89,6 +89,21 @@ void test_project() { ok(true, "expected constructor to succeed"); } +void test_project_metadata() { + auto localStoragePath = w_string_to_json("/absolute/path"); + expect_query_parse_error( + json_object({{"local-storage-path", localStoragePath}, + {"project", w_string_to_json("relative/path")}, + {"project-metadata", json_integer(5)}}), + "failed to parse query: 'project-metadata' must be a string"); + LocalSavedStateInterface interface( + json_object({{"local-storage-path", localStoragePath}, + {"project", w_string_to_json("foo")}, + {"project-metadata", w_string_to_json("meta")}}), + nullptr); + ok(true, "expected constructor to succeed"); +} + void test_path() { auto localStoragePath = w_string_to_json("/absolute/path"); LocalSavedStateInterface interface( @@ -101,13 +116,25 @@ void test_path() { "Expected path to be \"%s\" but observed \"%s\"", expectedPath, path.c_str()); + interface = LocalSavedStateInterface( + json_object({{"local-storage-path", localStoragePath}, + {"project", w_string_to_json("foo")}, + {"project-metadata", w_string_to_json("meta")}}), + nullptr); + path = interface.getLocalPath("hash"); + expectedPath = "/absolute/path/foo/hash_meta"; + ok(!strcmp(path.c_str(), expectedPath), + "Expected path to be \"%s\" but observed \"%s\"", + expectedPath, + path.c_str()); } int main(int, char**) { - plan_tests(22); + plan_tests(26); test_max_commits(); test_localStoragePath(); test_project(); + test_project_metadata(); test_path(); return exit_status(); diff --git a/tests/integration/test_local_saved_state.py b/tests/integration/test_local_saved_state.py index 8aaf4b58557c..9969bd50e87a 100644 --- a/tests/integration/test_local_saved_state.py +++ b/tests/integration/test_local_saved_state.py @@ -119,14 +119,20 @@ def getQuery(self, config): }, } + def getLocalFilename(self, saved_state_rev, metadata): + if metadata: + return saved_state_rev + "_" + metadata + return saved_state_rev + # Creates a saved state (with no content) for the specified project at the # specified bookmark within the specified local storage path. - def saveState(self, project, bookmark, local_storage): + def saveState(self, project, bookmark, local_storage, metadata=None): saved_state_rev = self.resolveCommitHash(bookmark, cwd=self.root) project_dir = os.path.join(local_storage, project) if not os.path.isdir(project_dir): os.mkdir(project_dir) - self.touchRelative(project_dir, saved_state_rev) + filename = self.getLocalFilename(saved_state_rev, metadata) + self.touchRelative(project_dir, filename) return saved_state_rev def getConfig(self, result): @@ -229,6 +235,70 @@ def test_localSavedStateLookupSuccess(self): self.assertSavedStateInfo(res, expected_path, saved_state_rev_feature3) self.assertFileListsEqual(res["files"], ["f1", "bar", "car"]) + def test_localSavedStateLookupSuccessWithMetadata(self): + local_storage = self.mkdtemp() + metadata = "metadata" + saved_state_rev_feature3 = self.saveState( + "example_project", "feature3", local_storage, metadata + ) + self.saveState("example_project", "feature0", local_storage, metadata) + config = { + "local-storage-path": local_storage, + "project": "example_project", + "project-metadata": metadata, + "max-commits": 10, + } + test_query = self.getQuery(config) + res = self.watchmanCommand("query", self.root, test_query) + expected_mergebase = self.resolveCommitHash("TheMaster", cwd=self.root) + self.assertMergebaseEquals(res, expected_mergebase) + self.assertStorageTypeLocal(res) + self.assertCommitIDEquals(res, saved_state_rev_feature3) + self.assertEqual(self.getConfig(res), config) + project_dir = os.path.join(local_storage, "example_project") + filepath = self.getLocalFilename(saved_state_rev_feature3, metadata) + expected_path = os.path.join(project_dir, filepath) + self.assertSavedStateInfo(res, expected_path, saved_state_rev_feature3) + self.assertFileListsEqual(res["files"], ["f1", "bar", "car"]) + + def test_localSavedStateFailureIfMetadataDoesNotMatch(self): + local_storage = self.mkdtemp() + self.saveState("example_project", "feature3", local_storage) + self.saveState("example_project", "feature0", local_storage) + config = { + "local-storage-path": local_storage, + "project": "example_project", + "project-metadata": "meta", + "max-commits": 10, + } + test_query = self.getQuery(config) + res = self.watchmanCommand("query", self.root, test_query) + self.assertSavedStateErrorEquals(res, "No suitable saved state found") + self.assertEqual(self.getConfig(res), config) + self.assertStorageTypeLocal(res) + expected_mergebase = self.resolveCommitHash("TheMaster", cwd=self.root) + self.assertMergebaseEquals(res, expected_mergebase) + self.assertFileListsEqual(res["files"], ["foo", "p1", "m2", "bar", "car", "f1"]) + + def test_localSavedStateFailureIfNoMetadataForFileThatHasIt(self): + local_storage = self.mkdtemp() + metadata = "metadata" + self.saveState("example_project", "feature3", local_storage, metadata) + self.saveState("example_project", "feature0", local_storage, metadata) + config = { + "local-storage-path": local_storage, + "project": "example_project", + "max-commits": 10, + } + test_query = self.getQuery(config) + res = self.watchmanCommand("query", self.root, test_query) + self.assertSavedStateErrorEquals(res, "No suitable saved state found") + self.assertEqual(self.getConfig(res), config) + self.assertStorageTypeLocal(res) + expected_mergebase = self.resolveCommitHash("TheMaster", cwd=self.root) + self.assertMergebaseEquals(res, expected_mergebase) + self.assertFileListsEqual(res["files"], ["foo", "p1", "m2", "bar", "car", "f1"]) + def test_localSavedStateSubscription(self): local_storage = self.mkdtemp() saved_state_rev_feature3 = self.saveState(