Check there is no changes matched by rules with upper limit revision
authorDmitry Neverov <dmitry.neverov@gmail.com>
Wed, 11 Apr 2018 08:30:41 +0000 (10:30 +0200)
committerDmitry Neverov <dmitry.neverov@gmail.com>
Wed, 11 Apr 2018 08:30:41 +0000 (10:30 +0200)
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/GitFacade.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/NativeGitFacade.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/UpdaterImpl.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/command/DiffCommand.java [new file with mode: 0644]
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/command/impl/DiffCommandImpl.java [new file with mode: 0644]
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/DiffWithUpperLimitRevisionTest.java [new file with mode: 0644]
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/builders/AgentRunningBuildBuilder.java
git-tests/src/native-git-testng.xml

index 0ce41db55c12682c176c482c35cccc1de4e320f6..ecc09c21251eee6a7a689deab8d4b6b0ce9cd9ff 100644 (file)
@@ -106,4 +106,7 @@ public interface GitFacade {
 
   @NotNull
   UpdateIndexCommand updateIndex();
+
+  @NotNull
+  DiffCommand diff();
 }
index 898f52a32536672bac02bec86ba96d2f91f90143..f2cd73dff9bc5a13a789397dfd89d7a98621258c 100644 (file)
@@ -215,6 +215,12 @@ public class NativeGitFacade implements GitFacade {
     return new UpdateIndexCommandImpl(createCommandLine());
   }
 
+  @NotNull
+  @Override
+  public DiffCommand diff() {
+    return new DiffCommandImpl(createCommandLine());
+  }
+
   @NotNull
   public Branches listBranches() throws VcsException {
     GitCommandLine cmd = createCommandLine();
index 81b6cdd681e8405f9a3d9f407cd8645c07c184a1..189b74c90f73a65338f61c6f440ebd145d17778c 100644 (file)
@@ -17,6 +17,7 @@
 package jetbrains.buildServer.buildTriggers.vcs.git.agent;
 
 import com.intellij.openapi.util.Trinity;
+import jetbrains.buildServer.BuildProblemData;
 import jetbrains.buildServer.agent.AgentRunningBuild;
 import jetbrains.buildServer.agent.BuildDirectoryCleanerCallback;
 import jetbrains.buildServer.agent.BuildProgressLogger;
@@ -30,6 +31,7 @@ import jetbrains.buildServer.buildTriggers.vcs.git.agent.errors.GitIndexCorrupte
 import jetbrains.buildServer.buildTriggers.vcs.git.agent.errors.GitOutdatedIndexException;
 import jetbrains.buildServer.log.Loggers;
 import jetbrains.buildServer.util.FileUtil;
+import jetbrains.buildServer.util.StringUtil;
 import jetbrains.buildServer.vcs.*;
 import org.apache.log4j.Logger;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -43,10 +45,7 @@ import java.io.File;
 import java.io.FileFilter;
 import java.io.IOException;
 import java.net.URISyntaxException;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.regex.Matcher;
 
 import static com.intellij.openapi.util.text.StringUtil.isEmpty;
@@ -127,6 +126,7 @@ public class UpdaterImpl implements Updater {
     logSshOptions(myPluginConfig.getGitVersion());
     checkAuthMethodIsSupported();
     doUpdate();
+    checkNoDiffWithUpperLimitRevision();
   }
 
   private void logSshOptions(@NotNull GitVersion gitVersion) {
@@ -1047,4 +1047,111 @@ public class UpdaterImpl implements Updater {
       }
     }
   }
+
+
+  private void checkNoDiffWithUpperLimitRevision() {
+    if ("false".equals(myBuild.getSharedConfigParameters().get("teamcity.git.checkDiffWithUpperLimitRevision"))) {
+      return;
+    }
+
+    String upperLimitRevision = getUpperLimitRevision();
+    if (upperLimitRevision == null) {
+      return;
+    }
+
+    String message = "Check no diff with upper limit revision " + upperLimitRevision;
+    myLogger.activityStarted(message, GitBuildProgressLogger.GIT_PROGRESS_ACTIVITY);
+    try {
+      if (!ensureCommitLoaded(upperLimitRevision)) {
+        myLogger.warning("Failed to fetch " + upperLimitRevision + ", will not analyze diff with upper limit revision");
+        return;
+      }
+      List<String> pathsMatchedByRules = getChangedFilesMatchedByRules(upperLimitRevision);
+      if (!pathsMatchedByRules.isEmpty()) {
+        StringBuilder msg = new StringBuilder();
+        msg.append("Files matched by checkout rules changed between build revision and upper-limit revision\n");
+        msg.append("Checkout rules: '").append(myRules.getAsString()).append("'\n");
+        msg.append("Build revision: '").append(myRevision).append("'\n");
+        msg.append("Upper limit revision: '").append(upperLimitRevision).append("'\n");
+        msg.append("Files:\n");
+        for (String path : pathsMatchedByRules) {
+          msg.append("\t").append(path).append("\n");
+        }
+        myLogger.error(msg.toString());
+        String type = "UpperLimitRevisionDiff";
+        myLogger.logBuildProblem(BuildProblemData.createBuildProblem(type + myRoot.getId(), type, "Diff with upper limit revision found"));
+      }
+    } finally {
+      myLogger.activityFinished(message, GitBuildProgressLogger.GIT_PROGRESS_ACTIVITY);
+    }
+  }
+
+  private boolean ensureCommitLoaded(@NotNull String commit) {
+    if (hasRevision(myTargetDirectory, commit))
+      return true;
+    try {
+      fetchAllBranches();
+    } catch (VcsException e) {
+      LOG.warn("Error while fetching commit " + commit, e);
+      return false;
+    }
+    return hasRevision(myTargetDirectory, commit);
+  }
+
+  @NotNull
+  private List<String> getChangedFilesMatchedByRules(@NotNull String upperLimitRevision) {
+    List<String> pathsMatchedByRules = new ArrayList<String>();
+    List<String> changedFiles = getChangedFiles(upperLimitRevision);
+    for (String file : changedFiles) {
+      if (myRules.map(file) != null) {
+        pathsMatchedByRules.add(file);
+      }
+    }
+    return pathsMatchedByRules;
+  }
+
+  @NotNull
+  private List<String> getChangedFiles(@NotNull String upperLimitRevision) {
+    try {
+      return myGitFactory.create(myTargetDirectory).diff()
+        .setFormat("--name-only")
+        .setCommit1(myRevision)
+        .setCommit2(upperLimitRevision)
+        .call();
+    } catch (VcsException e) {
+      myLogger.warning("Error while computing changed files between build and upper limit revisions: " + e.toString());
+      return Collections.emptyList();
+    }
+  }
+
+  @Nullable
+  private String getUpperLimitRevision() {
+    String rootExtId = getVcsRootExtId();
+    return rootExtId != null ? myBuild.getSharedConfigParameters().get("teamcity.upperLimitRevision." + rootExtId) : null;
+  }
+
+  @Nullable
+  private String getVcsRootExtId() {
+    // We don't have vcs root extId on the agent, deduce it from vcs.number parameters
+    String revisionParamPrefix = "build.vcs.number.";
+    String vcsRootExtId = null;
+    Map<String, String> params = myBuild.getSharedConfigParameters();
+    for (Map.Entry<String, String> param : params.entrySet()) {
+      if (param.getKey().startsWith(revisionParamPrefix) && myRevision.equals(param.getValue())) {
+        String extId = param.getKey().substring(revisionParamPrefix.length());
+        if (StringUtil.isNotEmpty(extId) && Character.isDigit(extId.charAt(0))) {
+          // We have build.vcs.number.<extId> and build.vcs.number.<root number>, ignore the latter (extId cannot start with digit)
+          continue;
+        }
+        if (vcsRootExtId != null) {
+          LOG.debug("Build has more than one VCS root with same revision " + myRevision + ": " + vcsRootExtId + " and " +
+                    extId + ", cannot deduce VCS root extId");
+          return null;
+        } else {
+          vcsRootExtId = extId;
+        }
+      }
+    }
+    return vcsRootExtId;
+  }
 }
diff --git a/git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/command/DiffCommand.java b/git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/command/DiffCommand.java
new file mode 100644 (file)
index 0000000..353bd19
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2000-2018 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.git.agent.command;
+
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public interface DiffCommand {
+
+  @NotNull
+  DiffCommand setCommit1(@NotNull String commit1);
+
+  @NotNull
+  DiffCommand setCommit2(@NotNull String commit2);
+
+  @NotNull
+  DiffCommand setFormat(@NotNull String format);
+
+  @NotNull
+  List<String> call() throws VcsException;
+
+}
diff --git a/git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/command/impl/DiffCommandImpl.java b/git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/command/impl/DiffCommandImpl.java
new file mode 100644 (file)
index 0000000..6f8b051
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2000-2018 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.git.agent.command.impl;
+
+import jetbrains.buildServer.ExecResult;
+import jetbrains.buildServer.buildTriggers.vcs.git.agent.GitCommandLine;
+import jetbrains.buildServer.buildTriggers.vcs.git.agent.command.DiffCommand;
+import jetbrains.buildServer.util.StringUtil;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class DiffCommandImpl extends BaseCommandImpl implements DiffCommand {
+
+  private String myCommit1;
+  private String myCommit2;
+  private String myFormat;
+
+  public DiffCommandImpl(@NotNull GitCommandLine cmd) {
+    super(cmd);
+  }
+
+  @NotNull
+  @Override
+  public DiffCommand setCommit1(@NotNull final String commit1) {
+    myCommit1 = commit1;
+    return this;
+  }
+
+  @NotNull
+  @Override
+  public DiffCommand setCommit2(@NotNull final String commit2) {
+    myCommit2 = commit2;
+    return this;
+  }
+
+  @NotNull
+  @Override
+  public DiffCommand setFormat(@NotNull final String format) {
+    myFormat = format;
+    return this;
+  }
+
+  @NotNull
+  @Override
+  public List<String> call() throws VcsException {
+    GitCommandLine cmd = getCmd();
+    cmd.addParameter("diff");
+    if (myFormat != null) {
+      cmd.addParameter(myFormat);
+    }
+    if (myCommit1 != null) {
+      cmd.addParameter(myCommit1);
+    }
+    if (myCommit2 != null) {
+      cmd.addParameter(myCommit2);
+    }
+
+    ExecResult r = CommandUtil.runCommand(cmd);
+    return Arrays.asList(StringUtil.splitByLines(r.getStdout().trim()));
+  }
+}
diff --git a/git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/DiffWithUpperLimitRevisionTest.java b/git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/DiffWithUpperLimitRevisionTest.java
new file mode 100644 (file)
index 0000000..b4d2c25
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2000-2018 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.git.tests;
+
+import jetbrains.buildServer.BuildProblemData;
+import jetbrains.buildServer.agent.AgentRunningBuild;
+import jetbrains.buildServer.agent.BuildProgressLogger;
+import jetbrains.buildServer.agent.FlowLogger;
+import jetbrains.buildServer.agent.NullBuildProgressLogger;
+import jetbrains.buildServer.buildTriggers.vcs.git.agent.GitAgentVcsSupport;
+import jetbrains.buildServer.buildTriggers.vcs.git.agent.PluginConfigImpl;
+import jetbrains.buildServer.messages.BuildMessage1;
+import jetbrains.buildServer.messages.Status;
+import jetbrains.buildServer.vcs.CheckoutRules;
+import jetbrains.buildServer.vcs.VcsException;
+import jetbrains.buildServer.vcs.VcsRoot;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.util.*;
+
+import static jetbrains.buildServer.buildTriggers.vcs.git.tests.GitVersionProvider.getGitPath;
+import static jetbrains.buildServer.buildTriggers.vcs.git.tests.VcsRootBuilder.vcsRoot;
+import static jetbrains.buildServer.buildTriggers.vcs.git.tests.builders.AgentRunningBuildBuilder.runningBuild;
+import static jetbrains.buildServer.util.Util.map;
+import static org.assertj.core.api.BDDAssertions.then;
+
+@Test
+public class DiffWithUpperLimitRevisionTest extends BaseRemoteRepositoryTest {
+
+  private GitAgentVcsSupport myVcsSupport;
+  private File myCheckoutDir;
+  private VcsRoot myRoot;
+  private BuildLogger myBuildLogger;
+
+  public DiffWithUpperLimitRevisionTest() {
+    super("repo.git");
+  }
+
+
+  @Override
+  @BeforeMethod
+  public void setUp() throws Exception {
+    super.setUp();
+
+    myCheckoutDir = myTempFiles.createTempDir();
+    myVcsSupport = new AgentSupportBuilder(myTempFiles).build();
+    String pathToGit = getGitPath();
+    myRoot = vcsRoot()
+      .withAgentGitPath(pathToGit)
+      .withFetchUrl(getRemoteRepositoryUrl("repo.git"))
+      .withBranch("master")
+      .build();
+    myBuildLogger = new BuildLogger();
+  }
+
+
+  public void no_error_if_upper_limit_revision_param_is_missing() throws Exception {
+    String version = "465ad9f630e451b9f2b782ffb09804c6a98c4bb9";
+    AgentRunningBuild build = createBuild(version, null);
+    myVcsSupport.updateSources(myRoot, new CheckoutRules("+:dir"), version, myCheckoutDir, build, false);
+  }
+
+
+  public void error_if_diff_found() throws Exception {
+    String version = "ad4528ed5c84092fdbe9e0502163cf8d6e6141e7";
+    AgentRunningBuild build = createBuild(version, "465ad9f630e451b9f2b782ffb09804c6a98c4bb9");
+    myVcsSupport.updateSources(myRoot, new CheckoutRules("+:dir"), version, myCheckoutDir, build, false);
+    then(myBuildLogger.getErrors()).isNotEmpty();
+    then(myBuildLogger.getErrors().iterator().next()).contains("Files matched by checkout rules changed between build revision and upper-limit revision");
+  }
+
+
+  public void can_be_disabled() throws Exception {
+    String version = "ad4528ed5c84092fdbe9e0502163cf8d6e6141e7";
+    AgentRunningBuild build = createBuild(version, "465ad9f630e451b9f2b782ffb09804c6a98c4bb9", "teamcity.git.checkDiffWithUpperLimitRevision", "false");
+    myVcsSupport.updateSources(myRoot, new CheckoutRules("+:dir"), version, myCheckoutDir, build, false);
+    then(myBuildLogger.getErrors()).isEmpty();
+  }
+
+
+  private AgentRunningBuild createBuild(@NotNull String buildRevision, @Nullable String upperLimitRevision, String... additionalParams) {
+    String rootExtId = "RootExtId";
+    Map<String, String> params = new HashMap<>();
+    params.put(PluginConfigImpl.USE_SPARSE_CHECKOUT, "true");
+    params.put("build.vcs.number." + rootExtId, buildRevision);
+    params.put("build.vcs.number.1", buildRevision);
+    if (upperLimitRevision != null) {
+      params.put("teamcity.upperLimitRevision." + rootExtId, upperLimitRevision);
+    }
+    params.putAll(map(additionalParams));
+    return runningBuild().sharedConfigParams(params).withBuildLogger(myBuildLogger).build();
+  }
+
+
+  private class BuildLogger extends NullBuildProgressLogger {
+    private List<String> myErrors = new ArrayList<>();
+
+    @NotNull
+    List<String> getErrors() {
+      return myErrors;
+    }
+
+    @Override
+    public void error(final String message) {
+      myErrors.add(message);
+    }
+  }
+}
index 552e571258f1b6d0da9e3cd086a924037124125b..f7bc5423f767808fe9e8ee198cd1cfbc5926a87d 100644 (file)
@@ -42,11 +42,16 @@ public class AgentRunningBuildBuilder {
   private Map<String, String> mySharedConfigParameters = new HashMap<String, String>();
   private Map<String, String> mySharedBuildParameters = new HashMap<String, String>();
   private List<VcsRootEntry> myRootEntries = null;
+  private BuildProgressLogger myBuildLogger = null;
 
   public static AgentRunningBuildBuilder runningBuild() {
     return new AgentRunningBuildBuilder();
   }
 
+  public AgentRunningBuildBuilder withBuildLogger(BuildProgressLogger logger) {
+    myBuildLogger = logger;
+    return this;
+  }
 
   public AgentRunningBuildBuilder sharedConfigParams(String... params) {
     mySharedConfigParameters.putAll(map(params));
@@ -98,7 +103,7 @@ public class AgentRunningBuildBuilder {
 
       @NotNull
       public BuildProgressLogger getBuildLogger() {
-        return new NullBuildProgressLogger();
+        return myBuildLogger != null ? myBuildLogger : new NullBuildProgressLogger();
       }
 
       @NotNull
index 785772cbfca6c8c214ba515ba79d98c46724e17a..d8dbe517c8e943535b30b9e8fc6a31bcb2b6fff5 100644 (file)
@@ -26,6 +26,7 @@
       <class name="jetbrains.buildServer.buildTriggers.vcs.git.tests.HttpUrlWithUsernameTest"/>
       <class name="jetbrains.buildServer.buildTriggers.vcs.git.tests.RemoteRepositoryConfiguratorTest"/>
       <class name="jetbrains.buildServer.buildTriggers.vcs.git.tests.AutoCheckoutTest"/>
+      <class name="jetbrains.buildServer.buildTriggers.vcs.git.tests.DiffWithUpperLimitRevisionTest"/>
     </classes>
   </test>
 </suite>
\ No newline at end of file