Repository: mozilla/pontoon

Issue:
The repo last_synced_revisions value is not validated
Pontoon keeps track of the last synced revision identifier, and uses this when [determining](https://github.com/mozilla/pontoon/blob/bfc3f8e0d6748aafd71f8e8155a517308fa2bbc3/pontoon/sync/vcs/repositories.py#L458) what files have changed. However, if the repo has changed and that commit/changeset id is no longer valid, the current fallback behaviour is to swallow the error and to return an empty list. This means that any changed or removed files won't be detected and updated until they next change.

On error, we should probably default to returning all the files, which is what's done if the revision id is missing.


Discussion:

Resolving patch:
diff --git a/pontoon/sync/vcs/repositories.py b/pontoon/sync/vcs/repositories.py
--- a/pontoon/sync/vcs/repositories.py
+++ b/pontoon/sync/vcs/repositories.py
@@ -373,7 +373,7 @@ def normalize_revision(rev):
                 for line in output.split("\n")
                 if line and line[0] in statuses
             ]
-        return []
+        return None
 
     def get_removed_files(self, path, from_revision):
         return self.get_changed_files(path, from_revision, ("D",))
@@ -398,7 +398,7 @@ def get_changed_files(self, path, from_revision, statuses=None):
                 for line in output.split("\n")
                 if line and line[0] in statuses
             ]
-        return []
+        return None
 
     def get_removed_files(self, path, from_revision):
         return self.get_changed_files(path, from_revision, ("D",))
@@ -437,7 +437,7 @@ def get_changed_files(self, path, from_revision, statuses=None):
                 for line in output.split("\n")
                 if line and line[0] in statuses
             ]
-        return []
+        return None
 
     def get_removed_files(self, path, from_revision):
         return self.get_changed_files(path, self._strip(from_revision), ("R",))
@@ -459,18 +459,19 @@ def get_changed_files(repo_type, path, revision):
     """Return a list of changed files for the repository."""
     repo = VCSRepository.for_type(repo_type, path)
     log.info(f"Retrieving changed files for: {path}:{revision}")
+
+    if revision is not None:
+        changed = repo.get_changed_files(path, revision)
+        removed = repo.get_removed_files(path, revision)
+        if changed is not None and removed is not None:
+            return changed, removed
+
     # If there's no latest revision we should return all the files in the latest
     # version of repository
-    if revision is None:
-        paths = []
-        for root, _, files in os.walk(path):
-            for f in files:
-                if root[0] == "." or "/." in root:
-                    continue
-                paths.append(os.path.join(root, f).replace(path + "/", ""))
-        return paths, []
-
-    return (
-        repo.get_changed_files(path, revision),
-        repo.get_removed_files(path, revision),
-    )
+    paths = []
+    for root, _, files in os.walk(path):
+        for f in files:
+            if root[0] == "." or "/." in root:
+                continue
+            paths.append(os.path.join(root, f).replace(path + "/", ""))
+    return paths, []


Test patch:
diff --git a/pontoon/sync/tests/test_vcs.py b/pontoon/sync/tests/test_vcs.py
--- a/pontoon/sync/tests/test_vcs.py
+++ b/pontoon/sync/tests/test_vcs.py
@@ -69,7 +69,7 @@ def test_changed_files_error(self):
         with patch.object(
             self.vcsrepository, "execute", side_effect=self.execute_failure
         ) as mock_execute:
-            assert self.vcsrepository.get_changed_files("path", "1") == []
+            assert self.vcsrepository.get_changed_files("path", "1") is None
             assert mock_execute.called
 
     def test_removed_files(self):
@@ -87,7 +87,7 @@ def test_removed_files_error(self):
         with patch.object(
             self.vcsrepository, "execute", side_effect=self.execute_failure
         ) as mock_execute:
-            assert self.vcsrepository.get_removed_files("path", "1") == []
+            assert self.vcsrepository.get_removed_files("path", "1") is None
             assert mock_execute.called