changeset 1116:7b244aaead2e TW-83599

TW-83599: recognize "path contains illegal component" error and retry pull command with explicit revision
author Andreas Eisele <andreas.eisele@jetbrains.com>
date Mon, 09 Oct 2023 17:48:08 +0200
parents b76e313cdb07
children c1a251997254
files mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandResult.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/PullCommand.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/exception/PathIllegalComponentException.java mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ServerPluginConfig.java mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ServerPluginConfigImpl.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialSupportBuilder.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ServerPluginConfigBuilder.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandResultTest.java
diffstat 10 files changed, 181 insertions(+), 10 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandResult.java	Thu Sep 14 17:09:37 2023 +0200
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandResult.java	Mon Oct 09 17:48:08 2023 +0200
@@ -242,6 +242,7 @@
     checkMergeWithWorkDirAncestor(stderr);
     checkNothingToMerge(stderr);
     checkUnknownException(stderr);
+    checkPathIllegalComponent(stderr);
   }
 
   private void checkUnrelatedRepository(@NotNull final String stderr) throws UnrelatedRepositoryException {
@@ -298,6 +299,16 @@
       throw new UnknownMercurialException(stderr);
   }
 
+  private void checkPathIllegalComponent(@NotNull final String stderr) throws PathIllegalComponentException {
+    final String prefix = "abort: path contains illegal component: ";
+    int idx = stderr.indexOf(prefix);
+    if (idx != -1) {
+      int startIdx = idx + prefix.length();
+      final String component = stderr.substring(startIdx);
+      throw new PathIllegalComponentException(component);
+    }
+  }
+
   private static Set<Integer> setOf(Integer... ints) {
     return new HashSet<>(asList(ints));
   }
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/PullCommand.java	Thu Sep 14 17:09:37 2023 +0200
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/PullCommand.java	Mon Oct 09 17:48:08 2023 +0200
@@ -31,6 +31,8 @@
   private int myTimeout;
   private boolean myTraceback;
   private boolean myProfile;
+  private ChangeSet myChangeSet;
+  private String myNamedRevision;
 
   public PullCommand(@NotNull CommandSettings commandSettings,
                      @NotNull String hgPath,
@@ -64,6 +66,16 @@
     return this;
   }
 
+  public PullCommand withChangeSet(@NotNull ChangeSet changeSet) {
+    myChangeSet = changeSet;
+    return this;
+  }
+
+  public PullCommand withNamedRevision(@NotNull String namedRevision) {
+    myNamedRevision = namedRevision;
+    return this;
+  }
+
   public void call() throws VcsException {
     removeLocks();
 
@@ -84,6 +96,12 @@
       cli.setHasProgress(true);
     }
 
+    if (myChangeSet != null) {
+      cli.addParameters("--rev", myChangeSet.getId());
+    } else if (myNamedRevision != null) {
+      cli.addParameters("--rev", myNamedRevision);
+    }
+
     String pullUrl = myAuthSettings.getRepositoryUrlWithCredentials(myPullUrl);
     cli.addParameter(pullUrl);
     runCommand(cli, settings);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/exception/PathIllegalComponentException.java	Mon Oct 09 17:48:08 2023 +0200
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2000-2023 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception;
+
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+public class PathIllegalComponentException extends VcsException {
+
+  @NotNull private final String myComponent;
+
+  public PathIllegalComponentException(@NotNull String component) {
+    super("Path contains illegal component: " + component);
+    myComponent = component;
+  }
+
+  @NotNull
+  public String getComponent() {
+    return myComponent;
+  }
+}
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java	Thu Sep 14 17:09:37 2023 +0200
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java	Mon Oct 09 17:48:08 2023 +0200
@@ -19,6 +19,7 @@
 import jetbrains.buildServer.buildTriggers.vcs.AbstractVcsPropertiesProcessor;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.AbandonedTransactionFound;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.PathIllegalComponentException;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.UnrelatedRepositoryException;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.WrongSubrepoUrlException;
 import jetbrains.buildServer.log.Loggers;
@@ -539,8 +540,12 @@
     }
   }
 
+  private void syncRepository(@NotNull final HgVcsRoot root, @NotNull final ChangeSet cset) throws VcsException {
+    syncRepository(root, cset, false);
+  }
+
   /* clone the repo if it doesn't exist, pull the repo if it doesn't contain specified changeSet */
-  private void syncRepository(@NotNull final HgVcsRoot root, @NotNull final ChangeSet cset) throws VcsException {
+  private void syncRepository(@NotNull final HgVcsRoot root, @NotNull final ChangeSet cset, boolean explicitRevision) throws VcsException {
     File workingDir = getWorkingDir(root);
     lockWorkDir(workingDir);
     HgRepo repo = createRepo(root);
@@ -552,14 +557,27 @@
       repo.setDefaultPath(root.getRepository());
       repo.setTeamCityConfig(root.getCustomHgConfigServer());
       try {
-        repo.pull().fromRepository(root.getRepository())
+        final PullCommand pullCommand = repo.pull().fromRepository(root.getRepository())
                 .withTimeout(myConfig.getPullTimeout())
-                .withProfile(myConfig.runWithProfile(root))
-                .call();
+                .withProfile(myConfig.runWithProfile(root));
+        if (explicitRevision) {
+          pullCommand.withChangeSet(cset);
+        }
+        pullCommand.call();
       } catch (UnrelatedRepositoryException e) {
         Loggers.VCS.warn("Repository at " + root.getRepository() + " is unrelated, clone it again");
         myMirrorManager.forgetDir(workingDir);
         syncRepository(root, cset);
+      } catch (PathIllegalComponentException e) {
+        final StringBuilder logMessage = new StringBuilder("Pulling from repository at ").append(root.getRepository())
+                .append(" failed because of illegal path component ").append(e.getComponent()).append(".");
+        if (myConfig.retryPullForIllegalPathComponent() && !explicitRevision) {
+          logMessage.append(" Retrying with explicit pull for revision ").append(cset).append(".");
+          Loggers.VCS.warn(logMessage.toString());
+          syncRepository(root, cset, true);
+          return;
+        }
+        Loggers.VCS.warn(logMessage.toString());
       }
     } finally {
       unlockWorkDir(workingDir);
@@ -581,11 +599,12 @@
   public <T> T syncRepository(@NotNull HgVcsRoot root, @NotNull SyncSettings<T> settings, @Nullable OperationContext context) throws VcsException {
     boolean customWorkingDir = root.getCustomWorkingDir() != null;
     File workingDir = getWorkingDir(root);
+    boolean pullFromTip = false;
     int attemptsLeft = 3;
     VcsException lastError = null;
     while (attemptsLeft-- > 0) {
       try {
-        return syncRepositoryOnce(root, settings, workingDir, context);
+        return syncRepositoryOnce(root, settings, workingDir, context, pullFromTip);
       } catch (UnrelatedRepositoryException e) {
         if (customWorkingDir)
           throw new VcsException(e.getMessage() + ". VCS root uses a custom clone dir, manual recovery is required.", e);
@@ -600,13 +619,26 @@
                 + workingDir.getAbsolutePath() + ". Clone it again, attempts left " + attemptsLeft);
         myMirrorManager.forgetDir(workingDir);
         lastError = e;
+      } catch (PathIllegalComponentException e) {
+        final StringBuilder logMessage = new StringBuilder("Pull failed because of illegal path component ").append(e.getComponent())
+                .append(" for repository ").append(root.getRepository()).append(" at ").append(workingDir.getAbsolutePath()).append(".");
+        if (myConfig.retryPullForIllegalPathComponent()) {
+          logMessage.append(" Retrying with pull from tip revision, attempts left ").append(attemptsLeft);
+          pullFromTip = true;
+        }
+        Loggers.VCS.warn(logMessage.toString());
+        lastError = e;
       }
     }
     throw lastError;
   }
 
 
-  private <T> T syncRepositoryOnce(@NotNull HgVcsRoot root, @NotNull SyncSettings<T> settings, @NotNull File workingDir, @Nullable OperationContext context) throws VcsException {
+  private <T> T syncRepositoryOnce(@NotNull HgVcsRoot root,
+                                   @NotNull SyncSettings<T> settings,
+                                   @NotNull File workingDir,
+                                   @Nullable OperationContext context,
+                                   boolean fromTip) throws VcsException {
     lockWorkDir(workingDir, settings.getProgressConsumer());
     HgRepo repo = context != null ? context.createRepo(root) : createRepo(root);
     try {
@@ -615,10 +647,13 @@
       repo.setDefaultPath(root.getRepository());
       repo.setTeamCityConfig(root.getCustomHgConfigServer());
       resetBookmarks(repo);
-      repo.pull().fromRepository(root.getRepository())
+      final PullCommand pullCommand = repo.pull().fromRepository(root.getRepository())
               .withTimeout(myConfig.getPullTimeout())
-              .withProfile(myConfig.runWithProfile(root))
-              .call();
+              .withProfile(myConfig.runWithProfile(root));
+      if (fromTip) {
+        pullCommand.withNamedRevision("tip");
+      }
+      pullCommand.call();
       return settings.getCmd().call();
     } finally {
       unlockWorkDir(workingDir);
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ServerPluginConfig.java	Thu Sep 14 17:09:37 2023 +0200
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ServerPluginConfig.java	Mon Oct 09 17:48:08 2023 +0200
@@ -61,4 +61,6 @@
   boolean runWithProfile(@NotNull HgVcsRoot root);
 
   boolean computeFromRevisions();
+
+  boolean retryPullForIllegalPathComponent();
 }
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ServerPluginConfigImpl.java	Thu Sep 14 17:09:37 2023 +0200
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ServerPluginConfigImpl.java	Mon Oct 09 17:48:08 2023 +0200
@@ -133,4 +133,8 @@
   public boolean computeFromRevisions() {
     return TeamCityProperties.getBooleanOrTrue("teamcity.hg.computeFromRevisions");
   }
+
+  public boolean retryPullForIllegalPathComponent() {
+    return TeamCityProperties.getBooleanOrTrue("teamcity.hg.retryPullForIllegalPathComponent");
+  }
 }
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialSupportBuilder.java	Thu Sep 14 17:09:37 2023 +0200
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialSupportBuilder.java	Mon Oct 09 17:48:08 2023 +0200
@@ -68,7 +68,6 @@
     return vcs;
   }
 
-
   public MercurialSupportBuilder withConfig(@NotNull ServerPluginConfig config) {
     myConfig = config;
     return this;
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java	Thu Sep 14 17:09:37 2023 +0200
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java	Mon Oct 09 17:48:08 2023 +0200
@@ -17,6 +17,7 @@
 
 import com.intellij.openapi.util.SystemInfo;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.PathIllegalComponentException;
 import jetbrains.buildServer.util.FileUtil;
 import jetbrains.buildServer.util.TestFor;
 import jetbrains.buildServer.vcs.*;
@@ -809,6 +810,26 @@
             CheckoutRules.DEFAULT);
   }
 
+  @TestFor(issues = "TW-83599")
+  public void should_retry_pull_on_illegal_path_component() throws Exception {
+    final VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
+    // we can't realiably force illegal path component errors with real hg repos
+    final RepoFactory failOnceRepoFactory = failOnceRepoFactory(new PathIllegalComponentException("some-component"));
+    myVcs = mercurialSupport().withConfig(myPluginConfig).withRepoFactory(failOnceRepoFactory).build();
+
+    myVcs.syncRepository(vcsRoot);
+  }
+
+  @TestFor(issues = "TW-83599")
+  public void get_content_should_retry_pull_on_illegal_path_component() throws IOException, VcsException {
+    VcsRootImpl vcsRoot = createVcsRoot(simpleRepo());
+    // we can't realiably force illegal path component errors with real hg repos
+    final RepoFactory failOnceRepoFactory = failOnceRepoFactory(new PathIllegalComponentException("some-component"));
+    myVcs = mercurialSupport().withConfig(myPluginConfig).withRepoFactory(failOnceRepoFactory).build();
+
+    byte[] content = myVcs.getContent("dir1/subdir/file2.txt", vcsRoot, "4:b06a290a363b");
+    assertEquals(new String(content), "bbb");
+  }
 
   private void assertFiles(final List<String> expectedFiles, final ModificationData modificationData) {
     Set<String> actualFiles = new HashSet<>();
@@ -838,5 +859,36 @@
   public void test_collect_changes_using_checkout_rules() {
     assertTrue(myVcs.getCollectChangesPolicy() instanceof CollectChangesByCheckoutRules);
   }
+
+  @NotNull
+  private RepoFactory failOnceRepoFactory(@NotNull VcsException failure) throws IOException {
+    final CommandSettingsForRootImpl commandSettingsFactory = new CommandSettingsForRootImpl(new TestCommandSettingsFactory(), new ExtensionsWeaver(), new CommandlineViaFileWrapperWeaver());
+
+    return new RepoFactory(myPluginConfig, commandSettingsFactory, myHgPathProvider) {
+      private boolean myDidFail = false;
+
+      @NotNull
+      @Override
+      public ServerHgRepo createRepo(@NotNull HgVcsRoot root, @NotNull File workingDir) throws VcsException {
+        if (myDidFail) {
+          return super.createRepo(root, workingDir);
+        } else {
+          myDidFail = true;
+          final CommandSettingsFactory rootSettingsFactory = commandSettingsFactory.forRoot(root);
+          return new ServerHgRepo(rootSettingsFactory, myConfig, workingDir, myHgPathProvider.getHgPath(root), root.getAuthSettings()) {
+            @Override
+            public PullCommand pull() {
+              return new PullCommand(rootSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings) {
+                @Override
+                public void call() throws VcsException {
+                  throw failure;
+                }
+              };
+            }
+          };
+        }
+      }
+    };
+  }
 }
 
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ServerPluginConfigBuilder.java	Thu Sep 14 17:09:37 2023 +0200
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/ServerPluginConfigBuilder.java	Mon Oct 09 17:48:08 2023 +0200
@@ -115,6 +115,10 @@
       public boolean computeFromRevisions() {
         return true;
       }
+
+      public boolean retryPullForIllegalPathComponent() {
+        return true;
+      }
     };
   }
 
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandResultTest.java	Thu Sep 14 17:09:37 2023 +0200
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandResultTest.java	Mon Oct 09 17:48:08 2023 +0200
@@ -21,6 +21,7 @@
 import jetbrains.buildServer.ExecResult;
 import jetbrains.buildServer.StreamGobbler;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.AbandonedTransactionFound;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.PathIllegalComponentException;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.UnknownRevisionException;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.UnrelatedRepositoryException;
 import jetbrains.buildServer.util.TestFor;
@@ -136,6 +137,16 @@
     }
   }
 
+  public void should_detect_illegal_path_component() throws VcsException {
+    final CommandResult commandResult = commandResultFor(execResult().withStderr("abort: path contains illegal component: /_mercurial_classpath_template.java.i"));
+    try {
+      commandResult.checkCommandFailed();
+      fail("expected PathIllegalComponentException to be thrown");
+    } catch (PathIllegalComponentException e) {
+      assertEquals("/_mercurial_classpath_template.java.i", e.getComponent());
+    }
+  }
+
   public void exception_should_not_contain_command_stdout_or_stderr() {
     final String stdout = "300Mb of output";
     final String stderr = "300Mb from stderr";